agent-tank 0.9.6 → 0.9.8
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/README.md +77 -34
- package/bin/agent-tank.js +61 -16
- package/package.json +5 -3
- package/src/activity-monitor.js +18 -7
- package/src/agents/agy.js +301 -0
- package/src/agents/base.js +27 -4
- package/src/agents/claude.js +39 -10
- package/src/agents/codex.js +1 -1
- package/src/agents/pty-output-parser.js +10 -4
- package/src/cli-background.js +198 -0
- package/src/client-auto-refresh-helpers.js +7 -7
- package/src/client-auto-refresh.js +2 -2
- package/src/client-monitor.js +2 -2
- package/src/discovery.js +9 -24
- package/src/history-store.js +1 -1
- package/src/icons.js +2 -2
- package/src/index.js +7 -6
- package/src/logger.js +2 -2
- package/src/pace-attachment.js +7 -7
- package/src/process-utils.js +215 -0
- package/src/public-status.js +5 -5
- package/src/snapshot-metrics.js +6 -6
- package/src/status-page.css +6 -6
- package/src/status-page.js +56 -38
- package/src/usage-formatters.js +7 -8
- package/src/agents/gemini-direct.js +0 -234
- package/src/agents/gemini.js +0 -463
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const { BaseAgent } = require('./base.js');
|
|
2
|
+
const { calculatePace } = require('../pace-evaluator.js');
|
|
3
|
+
const { CYCLE_DURATIONS } = require('../usage-formatters.js');
|
|
4
|
+
const logger = require('../logger.js');
|
|
5
|
+
|
|
6
|
+
class AgyAgent extends BaseAgent {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('agy', 'agy', ['--dangerously-skip-permissions']);
|
|
9
|
+
this._aboutSent = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getTimeout() {
|
|
13
|
+
return 35000; // Antigravity can take 10+ seconds to start and sign in
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getEnv() {
|
|
17
|
+
// Use xterm-256color for proper terminal support
|
|
18
|
+
// Antigravity CLI needs good terminal emulation for its TUI
|
|
19
|
+
return {
|
|
20
|
+
...process.env,
|
|
21
|
+
TERM: 'xterm-256color',
|
|
22
|
+
COLORTERM: 'truecolor',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_hasReadyPrompt(output) {
|
|
27
|
+
const clean = this.stripAnsi(output);
|
|
28
|
+
return /Antigravity CLI[\s\S]*\n>\s*(?:\n|$)/i.test(clean)
|
|
29
|
+
|| /Google AI Pro[\s\S]*\n>\s*(?:\n|$)/i.test(clean)
|
|
30
|
+
|| /Ready\s*\(/.test(clean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
isReadyForCommands(output) {
|
|
34
|
+
// Don't consider ready if authentication is in progress
|
|
35
|
+
if (this._isAuthenticating(output)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return this._hasReadyPrompt(output);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_isAuthenticating(output) {
|
|
42
|
+
// Detect OAuth/authentication screens where Escape would cancel the flow
|
|
43
|
+
const clean = this.stripAnsi(output);
|
|
44
|
+
const hasAuthFlow = /waiting for authentication/i.test(clean)
|
|
45
|
+
|| /signing in/i.test(clean)
|
|
46
|
+
|| /press esc.*to cancel/i.test(clean)
|
|
47
|
+
|| /authenticating\.\.\./i.test(clean);
|
|
48
|
+
|
|
49
|
+
if (!hasAuthFlow) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Footer hints can render before the actual input prompt is available.
|
|
54
|
+
// Treat auth as complete only when the real prompt has returned.
|
|
55
|
+
return !this._hasReadyPrompt(clean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
detectAuthenticationState(output) {
|
|
59
|
+
const clean = this.stripAnsi(output);
|
|
60
|
+
if (!clean) return null;
|
|
61
|
+
|
|
62
|
+
const hasAuthFailure = /failed to sign in\.\s*message:\s*([^\n]+)/i.test(clean)
|
|
63
|
+
|| /authentication consent could not be obtained/i.test(clean);
|
|
64
|
+
if (hasAuthFailure) {
|
|
65
|
+
const failureMatch = clean.match(/failed to sign in\.\s*message:\s*([^\n]+)/i);
|
|
66
|
+
const detail = failureMatch
|
|
67
|
+
? `Failed to sign in: ${failureMatch[1].trim()}`
|
|
68
|
+
: 'Antigravity CLI authentication failed.';
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
authenticated: false,
|
|
72
|
+
status: 'unauthenticated',
|
|
73
|
+
message: 'Authentication required',
|
|
74
|
+
detail,
|
|
75
|
+
action: 'Run agy in an interactive terminal to authenticate.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this._hasReadyPrompt(clean) || /signing in/i.test(clean)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const hasAuthMenu = /how would you like to authenticate for this project\?/i.test(clean)
|
|
84
|
+
|| /currently not signed in/i.test(clean);
|
|
85
|
+
|
|
86
|
+
if (!hasAuthMenu) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
authenticated: false,
|
|
92
|
+
status: 'unauthenticated',
|
|
93
|
+
message: 'Authentication required',
|
|
94
|
+
detail: 'Antigravity CLI is waiting for authentication.',
|
|
95
|
+
action: 'Run agy in an interactive terminal to authenticate.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
handleTrustPrompt(shell, output) {
|
|
100
|
+
// Check for trust prompt with various wordings
|
|
101
|
+
const trustPatterns = [
|
|
102
|
+
'Do you trust',
|
|
103
|
+
'trust the files',
|
|
104
|
+
'trust this folder',
|
|
105
|
+
'Trust this workspace',
|
|
106
|
+
'allow access',
|
|
107
|
+
'grant access',
|
|
108
|
+
'grant full access',
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
if (trustPatterns.some(pattern => output.toLowerCase().includes(pattern.toLowerCase()))) {
|
|
112
|
+
logger.agent(this.name, 'Detected trust prompt, auto-accepting...');
|
|
113
|
+
// Numbered menu with "1. Trust folder" pre-selected; Enter accepts it.
|
|
114
|
+
shell.write('1\r');
|
|
115
|
+
|
|
116
|
+
// Antigravity can redraw after trust; clear transitional UI before readiness.
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
if (!shell) return;
|
|
119
|
+
logger.agent(this.name, 'Post-trust: sending Escape to clear restart UI...');
|
|
120
|
+
shell.write('\x1b');
|
|
121
|
+
}, 3000);
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hasCompleteOutput(output) {
|
|
129
|
+
const clean = this.stripAnsi(output);
|
|
130
|
+
if (this.detectAuthenticationState(clean)) return true;
|
|
131
|
+
if (/Model\s+Quota/i.test(clean)) {
|
|
132
|
+
return /(?:Gemini|Claude|GPT)[\s\S]*\d+(?:\.\d+)?%/i.test(clean);
|
|
133
|
+
}
|
|
134
|
+
const hasModel = /(?:Gemini|Claude|GPT)[^\n]+/i.test(clean);
|
|
135
|
+
const hasPercent = /\d+(?:\.\d+)?%/i.test(clean);
|
|
136
|
+
return hasModel && hasPercent;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Convert duration string like "3h 26m" or "5d 2h" to seconds
|
|
140
|
+
parseDurationToSeconds(durationStr) {
|
|
141
|
+
if (!durationStr) return null;
|
|
142
|
+
|
|
143
|
+
let totalSeconds = 0;
|
|
144
|
+
|
|
145
|
+
// Match days
|
|
146
|
+
const daysMatch = durationStr.match(/(\d+)\s*d/i);
|
|
147
|
+
if (daysMatch) {
|
|
148
|
+
totalSeconds += parseInt(daysMatch[1]) * 24 * 60 * 60;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Match hours
|
|
152
|
+
const hoursMatch = durationStr.match(/(\d+)\s*h/i);
|
|
153
|
+
if (hoursMatch) {
|
|
154
|
+
totalSeconds += parseInt(hoursMatch[1]) * 60 * 60;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Match minutes
|
|
158
|
+
const minsMatch = durationStr.match(/(\d+)\s*m/i);
|
|
159
|
+
if (minsMatch) {
|
|
160
|
+
totalSeconds += parseInt(minsMatch[1]) * 60;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return totalSeconds > 0 ? totalSeconds : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
sendCommands(shell, output) {
|
|
167
|
+
// Check if authentication is still in progress - don't send commands that could interfere
|
|
168
|
+
if (this._isAuthenticating(output) || this.detectAuthenticationState(output)) {
|
|
169
|
+
logger.agent(this.name, 'Authentication required or in progress, cannot send /usage command');
|
|
170
|
+
// Don't retry - let the timeout handle it. Sending Escape would cancel auth.
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
logger.agent(this.name, 'Sending /usage command...');
|
|
174
|
+
setTimeout(() => shell.write('/usage'), 200);
|
|
175
|
+
setTimeout(() => shell.write('\r'), 600);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Calculate pace data for a model entry when reset timing is available.
|
|
179
|
+
_addPaceData(entry, resetsInSeconds) {
|
|
180
|
+
const cycleDuration = CYCLE_DURATIONS.sessionAgy;
|
|
181
|
+
if (!cycleDuration || resetsInSeconds == null) return;
|
|
182
|
+
const paceData = calculatePace({ usagePercent: entry.percentUsed, resetsInSeconds, cycleDurationSeconds: cycleDuration });
|
|
183
|
+
if (paceData) entry.pace = paceData;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
parseOutput(output) {
|
|
187
|
+
const clean = this.stripAnsi(output);
|
|
188
|
+
const usage = {
|
|
189
|
+
models: [],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
this._parseAgyUsage(clean, usage);
|
|
193
|
+
|
|
194
|
+
// Detect version update notification in output
|
|
195
|
+
const updateMatch = clean.match(/[Uu]pdate\s+available[^\d]*([\d.]+)\s*(?:→|->|to)\s*([\d.]+)/);
|
|
196
|
+
if (updateMatch) {
|
|
197
|
+
usage.version = { current: updateMatch[1], latest: updateMatch[2] };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return usage;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_parseAgyUsage(clean, usage) {
|
|
204
|
+
if (!/Model\s+Quota/i.test(clean)) return;
|
|
205
|
+
|
|
206
|
+
const lines = clean.split('\n').map(line => line.trim()).filter(Boolean);
|
|
207
|
+
const knownMarkers = new Set([
|
|
208
|
+
'Model Quota',
|
|
209
|
+
'Quota available',
|
|
210
|
+
'? for shortcuts',
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
const modelLine = lines[i];
|
|
215
|
+
if (knownMarkers.has(modelLine) || /^(?:[>└]|[-─↑/↓]|esc\s+to\s+cancel)/i.test(modelLine)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!/[A-Za-z]/.test(modelLine) || /\d+\s*%/.test(modelLine)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const percentLine = lines[i + 1] || '';
|
|
223
|
+
const percentMatch = percentLine.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
224
|
+
if (!percentMatch) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const usageLeft = parseFloat(percentMatch[1]);
|
|
229
|
+
const percentUsed = Number((100 - usageLeft).toFixed(1));
|
|
230
|
+
const resetLine = lines[i + 2] || '';
|
|
231
|
+
const resetMatch = resetLine.match(/(?:resets?|available)\s*(?:in|at)?\s*([0-9dhm\s]+)?/i);
|
|
232
|
+
const resetsIn = resetMatch?.[1]?.trim() || null;
|
|
233
|
+
const resetsInSeconds = resetsIn ? this.parseDurationToSeconds(resetsIn) : null;
|
|
234
|
+
|
|
235
|
+
if (!usage.models.find(m => m.model === modelLine)) {
|
|
236
|
+
const modelEntry = { model: modelLine, usageLeft, percentUsed, resetsIn, resetsInSeconds };
|
|
237
|
+
this._addPaceData(modelEntry, resetsInSeconds);
|
|
238
|
+
usage.models.push(modelEntry);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async fetchMetadata() {
|
|
244
|
+
if (this._aboutSent) {
|
|
245
|
+
return this.metadata;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._aboutSent = true;
|
|
249
|
+
return this._parseAgyMetadata(this.output);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_parseAgyMetadata(output) {
|
|
253
|
+
const clean = this.stripAnsi(output);
|
|
254
|
+
const metadata = {};
|
|
255
|
+
|
|
256
|
+
const versionMatch = clean.match(/Antigravity\s+CLI\s+v?([\d.]+)/i);
|
|
257
|
+
if (versionMatch) metadata.version = versionMatch[1];
|
|
258
|
+
|
|
259
|
+
const emailMatch = clean.match(/(\S+@\S+)/);
|
|
260
|
+
if (emailMatch) metadata.email = this.stripBoxChars(emailMatch[1]);
|
|
261
|
+
|
|
262
|
+
const cwdMatch = clean.match(/\n\s*(\/[^\n]+)\n/);
|
|
263
|
+
if (cwdMatch) metadata.cwd = this.stripBoxChars(cwdMatch[1]);
|
|
264
|
+
|
|
265
|
+
const modelMatch = clean.match(/((?:Gemini|Claude|GPT)[^\n]+?)\s+\((?:Google AI|[^)]*Pro|[^)]*Free|[^)]*)\)/i)
|
|
266
|
+
|| clean.match(/\?\s+for\s+shortcuts\s+(.+?)(?:\n|$)/i);
|
|
267
|
+
if (modelMatch) metadata.model = this.stripBoxChars(modelMatch[1]);
|
|
268
|
+
|
|
269
|
+
metadata.cli = 'agy';
|
|
270
|
+
return Object.keys(metadata).length > 1 ? metadata : { cli: 'agy' };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Lightweight keepalive to prevent session expiration.
|
|
275
|
+
* Sends escape key to maintain the PTY session without triggering API calls.
|
|
276
|
+
*
|
|
277
|
+
* @returns {Promise<boolean>} True if keepalive succeeded
|
|
278
|
+
*/
|
|
279
|
+
async keepalive() {
|
|
280
|
+
if (this.freshProcess) {
|
|
281
|
+
console.log(`[${this.name}] Keepalive skipped (fresh process mode)`);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!this.shell || !this.processReady) {
|
|
286
|
+
console.log(`[${this.name}] Keepalive: spawning process...`);
|
|
287
|
+
await this.spawnProcess();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (this.shell) {
|
|
291
|
+
console.log(`[${this.name}] Keepalive: sending ping...`);
|
|
292
|
+
// Send escape key to dismiss any UI and trigger activity
|
|
293
|
+
this.shell.write('\x1b');
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = { AgyAgent };
|
package/src/agents/base.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- Tracked in issue #90; keep this PR scoped to background startup behavior. */
|
|
2
|
+
|
|
1
3
|
const pty = require('node-pty');
|
|
2
4
|
const process = require('node:process');
|
|
3
5
|
const logger = require('../logger.js');
|
|
@@ -32,6 +34,25 @@ class BaseAgent {
|
|
|
32
34
|
return new Error('Agent stopping');
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Best-effort dump of raw output to a temp file for debugging.
|
|
39
|
+
* Never throws: the target may be unwritable (e.g. a stale file in
|
|
40
|
+
* /tmp owned by another user), and these dumps run inside timer
|
|
41
|
+
* callbacks where an exception would crash the whole process.
|
|
42
|
+
* @param {string} output Raw output to dump
|
|
43
|
+
* @returns {string|null} Path written to, or null on failure
|
|
44
|
+
*/
|
|
45
|
+
writeDebugOutput(output) {
|
|
46
|
+
const debugPath = require('node:path').join(require('node:os').tmpdir(), `${this.name}-output.txt`);
|
|
47
|
+
try {
|
|
48
|
+
require('fs').writeFileSync(debugPath, output);
|
|
49
|
+
return debugPath;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.agent(this.name, 'Could not write debug output:', logger.dim(err.message));
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
isStopping() {
|
|
36
57
|
return this._stopRequested;
|
|
37
58
|
}
|
|
@@ -308,7 +329,7 @@ class BaseAgent {
|
|
|
308
329
|
logger.agent(this.name, 'Command timeout after', logger.dim(`${this.getTimeout()}ms`), ', output length:', logger.dim(`${this.output.length}`));
|
|
309
330
|
if (this.output.length > 0) {
|
|
310
331
|
logger.agent(this.name, 'Partial output:', logger.dim(this.stripAnsi(this.output).substring(0, 500)));
|
|
311
|
-
|
|
332
|
+
this.writeDebugOutput(this.output);
|
|
312
333
|
}
|
|
313
334
|
// Kill the stuck process so it respawns fresh on the next refresh
|
|
314
335
|
logger.agent(this.name, 'Killing stuck process to force respawn');
|
|
@@ -351,7 +372,7 @@ class BaseAgent {
|
|
|
351
372
|
this.output += data;
|
|
352
373
|
|
|
353
374
|
// Always respond to terminal queries — suppressing them causes deadlocks
|
|
354
|
-
// when CLIs
|
|
375
|
+
// when CLIs wait for cursor position responses before
|
|
355
376
|
// rendering output. Delay slightly during command-in-flight to avoid
|
|
356
377
|
// interleaving with command text being sent via setTimeout.
|
|
357
378
|
if (this._commandInFlight) {
|
|
@@ -448,8 +469,10 @@ class BaseAgent {
|
|
|
448
469
|
logger.agent(this.name, 'Timeout after', logger.dim(`${timeout}ms`), ', output length:', logger.dim(`${output.length}`));
|
|
449
470
|
if (output.length > 0) {
|
|
450
471
|
logger.agent(this.name, 'Partial output:', logger.dim(this.stripAnsi(output).substring(0, 500)));
|
|
451
|
-
|
|
452
|
-
|
|
472
|
+
const debugPath = this.writeDebugOutput(output);
|
|
473
|
+
if (debugPath) {
|
|
474
|
+
logger.agent(this.name, 'Full output written to', logger.dim(debugPath));
|
|
475
|
+
}
|
|
453
476
|
}
|
|
454
477
|
shell.kill();
|
|
455
478
|
if (output.length > 100) {
|
package/src/agents/claude.js
CHANGED
|
@@ -209,20 +209,28 @@ class ClaudeAgent extends BaseAgent {
|
|
|
209
209
|
const hasSessionData = parsed.session && typeof parsed.session.percent === 'number';
|
|
210
210
|
const hasLegacyWeekly = parsed.weekly && typeof parsed.weekly.percent === 'number';
|
|
211
211
|
const hasAllModelsWeekly = parsed.weeklyAll && typeof parsed.weeklyAll.percent === 'number';
|
|
212
|
+
const hasSonnetWeekly = parsed.weeklySonnet && typeof parsed.weeklySonnet.percent === 'number';
|
|
212
213
|
|
|
213
214
|
// Newer Claude builds often emit usable session/weekly data before the UI fully settles.
|
|
214
|
-
//
|
|
215
|
-
return hasSessionData && (hasLegacyWeekly || hasAllModelsWeekly);
|
|
215
|
+
// For the all-model weekly format, wait for the separate Sonnet section too.
|
|
216
|
+
return Boolean(hasSessionData && (hasLegacyWeekly || (hasAllModelsWeekly && hasSonnetWeekly)));
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
sendCommands(shell, _output) {
|
|
219
220
|
logger.agent(this.name, 'Sending /usage command...');
|
|
220
221
|
// Claude keeps slash-command suggestions open for /usage on newer builds.
|
|
221
222
|
// Confirm the command selection, then submit the actual command execution.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
const writeIfActive = (value) => {
|
|
224
|
+
if (!shell) return;
|
|
225
|
+
try {
|
|
226
|
+
shell.write(value);
|
|
227
|
+
} catch (_err) {
|
|
228
|
+
// The persistent exit handler records the underlying process state.
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
setTimeout(() => writeIfActive('/usage'), 100);
|
|
232
|
+
setTimeout(() => writeIfActive('\r'), 500);
|
|
233
|
+
setTimeout(() => writeIfActive('\r'), 900);
|
|
226
234
|
}
|
|
227
235
|
|
|
228
236
|
// After getting /usage output, dismiss dialog so next refresh starts with clean prompt
|
|
@@ -286,6 +294,18 @@ class ClaudeAgent extends BaseAgent {
|
|
|
286
294
|
this._onDataCallback = null;
|
|
287
295
|
this._commandInFlight = false;
|
|
288
296
|
clearTimeout(timer);
|
|
297
|
+
if (this.shell && statusOutput) {
|
|
298
|
+
try {
|
|
299
|
+
this.shell.write('\x1b');
|
|
300
|
+
} catch (_err) {
|
|
301
|
+
// The persistent exit handler records the underlying process state.
|
|
302
|
+
}
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
this.output = '';
|
|
305
|
+
resolve(result);
|
|
306
|
+
}, 300);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
289
309
|
resolve(result);
|
|
290
310
|
};
|
|
291
311
|
|
|
@@ -306,10 +326,19 @@ class ClaudeAgent extends BaseAgent {
|
|
|
306
326
|
|
|
307
327
|
logger.agent(this.name, 'Sending /status command for metadata...');
|
|
308
328
|
this.output = '';
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
329
|
+
const writeIfActive = (value) => {
|
|
330
|
+
if (!this.shell) {
|
|
331
|
+
finish(this._parseStatusOutput(statusOutput));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
this.shell.write(value);
|
|
336
|
+
} catch (_err) {
|
|
337
|
+
finish(this._parseStatusOutput(statusOutput));
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
setTimeout(() => writeIfActive('/status'), 100);
|
|
341
|
+
setTimeout(() => writeIfActive('\r'), 500);
|
|
313
342
|
});
|
|
314
343
|
}
|
|
315
344
|
|
package/src/agents/codex.js
CHANGED
|
@@ -229,7 +229,7 @@ class CodexAgent extends BaseAgent {
|
|
|
229
229
|
logger.agent(this.name, 'Command timeout after', logger.dim(`${this.getTimeout()}ms`), ', output length:', logger.dim(`${this.output.length}`));
|
|
230
230
|
if (this.output.length > 0) {
|
|
231
231
|
logger.agent(this.name, 'Partial output:', logger.dim(this.stripAnsi(this.output).substring(0, 500)));
|
|
232
|
-
|
|
232
|
+
this.writeDebugOutput(this.output);
|
|
233
233
|
}
|
|
234
234
|
if (this.output.length > 100) {
|
|
235
235
|
finish(this.output);
|
|
@@ -125,6 +125,12 @@ function extractResetTime(section) {
|
|
|
125
125
|
// Look for date+time pattern FIRST (more specific)
|
|
126
126
|
const dateTimeMatch = section.match(/((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},?\s*\d{1,2}(?::\d{2})?\s*(am|pm))\s*\(([^)]+)\)/i);
|
|
127
127
|
if (dateTimeMatch) return `${dateTimeMatch[1]} (${dateTimeMatch[3]})`;
|
|
128
|
+
// Compact PTY output can lose the column spaces: "Jun10,6pm(Europe/Berlin)".
|
|
129
|
+
const compactDateTimeMatch = section.match(/((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))(\d{1,2}),?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(([^)]+)\)/i);
|
|
130
|
+
if (compactDateTimeMatch) {
|
|
131
|
+
const [, month, day, hour, minutes, ampm, tz] = compactDateTimeMatch;
|
|
132
|
+
return `${month} ${day}, ${hour}${minutes ? `:${minutes}` : ''}${ampm} (${tz})`;
|
|
133
|
+
}
|
|
128
134
|
// Try clean time-only pattern
|
|
129
135
|
const timeOnlyMatch = section.match(/(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i);
|
|
130
136
|
if (timeOnlyMatch) return `${timeOnlyMatch[1]} (${timeOnlyMatch[2]})`;
|
|
@@ -149,7 +155,7 @@ function extractResetTime(section) {
|
|
|
149
155
|
*/
|
|
150
156
|
function parseUsageSection(section) {
|
|
151
157
|
if (!section) return null;
|
|
152
|
-
const percentMatch = section.match(/(\d+)\s*%\s*
|
|
158
|
+
const percentMatch = section.match(/(\d+)\s*%\s*use?d/i);
|
|
153
159
|
if (!percentMatch) return null;
|
|
154
160
|
const resetsAt = extractResetTime(section);
|
|
155
161
|
const resetData = resetsAt ? parseResetTime(resetsAt) : null;
|
|
@@ -245,21 +251,21 @@ function parsePtyOutput(clean) {
|
|
|
245
251
|
const usage = { session: null, weeklyAll: null, weeklySonnet: null };
|
|
246
252
|
|
|
247
253
|
// Parse session
|
|
248
|
-
const sessionData = parseUsageSection(extractSection(clean, 'Current\\s
|
|
254
|
+
const sessionData = parseUsageSection(extractSection(clean, 'Current\\s*session', 'Current\\s*week'));
|
|
249
255
|
if (sessionData) {
|
|
250
256
|
usage.session = { label: 'Current session', ...sessionData };
|
|
251
257
|
addPaceData(usage.session, 'session');
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
// Parse weekly (all models)
|
|
255
|
-
const weeklyAllData = parseUsageSection(extractSection(clean, 'Current\\s
|
|
261
|
+
const weeklyAllData = parseUsageSection(extractSection(clean, 'Current\\s*week\\s*\\(?\\s*all\\s*models\\s*\\)?', 'Current\\s*week\\s*\\(?\\s*Son'));
|
|
256
262
|
if (weeklyAllData) {
|
|
257
263
|
usage.weeklyAll = { label: 'Current week (all models)', ...weeklyAllData };
|
|
258
264
|
addPaceData(usage.weeklyAll, 'weekly');
|
|
259
265
|
}
|
|
260
266
|
|
|
261
267
|
// Parse weekly (Sonnet only)
|
|
262
|
-
const weeklySonnetData = parseUsageSection(extractSection(clean, 'Current\\s
|
|
268
|
+
const weeklySonnetData = parseUsageSection(extractSection(clean, 'Current\\s*week\\s*\\(?\\s*Son\\w*\\s*\\w*ly\\s*\\)?', 'Extra\\s*usage|esc\\s*to\\s*cancel|Current\\s*week\\s*\\('));
|
|
263
269
|
if (weeklySonnetData) {
|
|
264
270
|
usage.weeklySonnet = { label: 'Current week (Sonnet only)', ...weeklySonnetData };
|
|
265
271
|
addPaceData(usage.weeklySonnet, 'weekly');
|