agent-tank 0.9.2 → 0.9.4
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 +313 -443
- package/bin/agent-tank.js +34 -6
- package/package.json +1 -1
- package/src/agents/base.js +110 -2
- package/src/agents/claude.js +10 -28
- package/src/agents/codex.js +3 -0
- package/src/client-auto-refresh.js +2 -2
- package/src/index.js +141 -10
- package/src/json-rpc-client.js +3 -0
- package/src/server.js +6 -3
- package/src/shutdown-handler.js +32 -0
- package/src/usage-formatters.js +3 -3
package/bin/agent-tank.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const { parseArgs } = require('node:util');
|
|
5
5
|
const { AgentTank } = require('../src/index.js');
|
|
6
|
+
const { installShutdownHandlers } = require('../src/shutdown-handler.js');
|
|
7
|
+
const pkg = require('../package.json');
|
|
6
8
|
|
|
7
9
|
const options = {
|
|
8
10
|
claude: { type: 'boolean', default: false },
|
|
@@ -11,12 +13,14 @@ const options = {
|
|
|
11
13
|
'claude-api': { type: 'boolean', default: false },
|
|
12
14
|
port: { type: 'string', default: '3456' },
|
|
13
15
|
host: { type: 'string' },
|
|
16
|
+
docker: { type: 'boolean', default: true },
|
|
14
17
|
'auth-user': { type: 'string' },
|
|
15
18
|
'auth-pass': { type: 'string' },
|
|
16
19
|
'auth-token': { type: 'string' },
|
|
17
20
|
'fresh-process': { type: 'boolean', default: false },
|
|
18
21
|
config: { type: 'string', short: 'c' },
|
|
19
22
|
help: { type: 'boolean', short: 'h', default: false },
|
|
23
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
20
24
|
'auto-discover': { type: 'boolean', default: true },
|
|
21
25
|
'auto-refresh': { type: 'boolean', default: true },
|
|
22
26
|
'auto-refresh-interval': { type: 'string', default: '60' },
|
|
@@ -50,12 +54,14 @@ Options:
|
|
|
50
54
|
--codex Enable Codex monitoring
|
|
51
55
|
--claude-api Use direct Anthropic API for Claude usage (faster, 60s refresh)
|
|
52
56
|
--port <port> HTTP server port (default: 3456)
|
|
53
|
-
--host <host> Bind address (default: 127.0.0.1)
|
|
57
|
+
--host <host> Bind address (default: 127.0.0.1 + Docker bridge when available)
|
|
58
|
+
--docker Enable Docker bridge bind when --host is omitted (default: true)
|
|
54
59
|
--auth-user <user> HTTP Basic Auth username
|
|
55
60
|
--auth-pass <pass> HTTP Basic Auth password
|
|
56
61
|
--auth-token <token> API key for Bearer token auth
|
|
57
62
|
--fresh-process Spawn a new process per refresh (default: false)
|
|
58
63
|
--config, -c Path to config file (JSON)
|
|
64
|
+
--version, -v Show version
|
|
59
65
|
--auto-discover Auto-discover available agents (default: true)
|
|
60
66
|
--auto-refresh Enable/disable background auto-refresh (default: true)
|
|
61
67
|
--auto-refresh-mode <mode> Refresh mode: none, interval, activity (default: activity)
|
|
@@ -79,6 +85,7 @@ Environment variables:
|
|
|
79
85
|
AGENT_TANK_PASS Basic auth password (overrides --auth-pass)
|
|
80
86
|
AGENT_TANK_TOKEN API key (overrides --auth-token)
|
|
81
87
|
AGENT_TANK_HOST Bind address (overrides --host)
|
|
88
|
+
AGENT_TANK_DOCKER Enable/disable Docker bridge bind ("1"/"true" or "0"/"false")
|
|
82
89
|
AGENT_TANK_FRESH_PROCESS Use fresh process per refresh ("1" or "true")
|
|
83
90
|
AGENT_TANK_CLAUDE_API Use direct Anthropic API for Claude usage ("1" or "true")
|
|
84
91
|
AGENT_TANK_AUTO_REFRESH Enable/disable background auto-refresh ("1" or "true" / "0" or "false")
|
|
@@ -94,6 +101,7 @@ Examples:
|
|
|
94
101
|
agent-tank --claude --gemini # Monitor specific agents
|
|
95
102
|
agent-tank --port 8080 # Use custom port
|
|
96
103
|
agent-tank --host 0.0.0.0 # Expose on all interfaces
|
|
104
|
+
agent-tank --no-docker # Bind localhost only when host is omitted
|
|
97
105
|
agent-tank --auth-user admin --auth-pass secret # Enable basic auth
|
|
98
106
|
agent-tank --auth-token mykey # Enable API key auth
|
|
99
107
|
agent-tank -c ./config.json # Use config file
|
|
@@ -127,6 +135,12 @@ async function main() {
|
|
|
127
135
|
return;
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
if (values.version) {
|
|
139
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
140
|
+
process.exitCode = 0;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
130
144
|
// Load config file if specified
|
|
131
145
|
let config = {};
|
|
132
146
|
if (values.config) {
|
|
@@ -151,6 +165,13 @@ async function main() {
|
|
|
151
165
|
};
|
|
152
166
|
|
|
153
167
|
const host = process.env.AGENT_TANK_HOST || values.host || config.host;
|
|
168
|
+
const dockerEnv = process.env.AGENT_TANK_DOCKER;
|
|
169
|
+
let dockerAccess = values.docker;
|
|
170
|
+
if (dockerEnv !== undefined) {
|
|
171
|
+
dockerAccess = dockerEnv === '1' || dockerEnv === 'true';
|
|
172
|
+
} else if (config.dockerAccess !== undefined) {
|
|
173
|
+
dockerAccess = config.dockerAccess;
|
|
174
|
+
}
|
|
154
175
|
|
|
155
176
|
const freshProcessEnv = process.env.AGENT_TANK_FRESH_PROCESS;
|
|
156
177
|
const freshProcess = values['fresh-process'] ||
|
|
@@ -241,6 +262,7 @@ async function main() {
|
|
|
241
262
|
autoDiscover: values['auto-discover'] && agents.length === 0,
|
|
242
263
|
port: parseInt(values.port || config.port || '3456', 10),
|
|
243
264
|
host,
|
|
265
|
+
dockerAccess,
|
|
244
266
|
auth,
|
|
245
267
|
freshProcess,
|
|
246
268
|
claudeApi,
|
|
@@ -255,15 +277,19 @@ async function main() {
|
|
|
255
277
|
});
|
|
256
278
|
|
|
257
279
|
// Graceful shutdown
|
|
258
|
-
|
|
280
|
+
let shuttingDown = false;
|
|
281
|
+
let cleanupShutdownHandlers = () => {};
|
|
282
|
+
const shutdown = (signal) => {
|
|
283
|
+
if (shuttingDown) return;
|
|
284
|
+
shuttingDown = true;
|
|
285
|
+
cleanupShutdownHandlers();
|
|
259
286
|
if (!jsonMode) {
|
|
260
|
-
originalLog(
|
|
287
|
+
originalLog(`\nShutting down${signal ? ` (${signal})` : ''}...`);
|
|
261
288
|
}
|
|
262
289
|
watcher.stop();
|
|
263
|
-
process.
|
|
290
|
+
process.exit(0);
|
|
264
291
|
};
|
|
265
|
-
|
|
266
|
-
process.on('SIGTERM', shutdown);
|
|
292
|
+
cleanupShutdownHandlers = installShutdownHandlers({ shutdown });
|
|
267
293
|
|
|
268
294
|
try {
|
|
269
295
|
if (onceMode) {
|
|
@@ -281,6 +307,7 @@ async function main() {
|
|
|
281
307
|
originalLog(JSON.stringify(agentStatus, null, 2));
|
|
282
308
|
}
|
|
283
309
|
}
|
|
310
|
+
cleanupShutdownHandlers();
|
|
284
311
|
watcher.stop();
|
|
285
312
|
process.exitCode = 0;
|
|
286
313
|
return;
|
|
@@ -288,6 +315,7 @@ async function main() {
|
|
|
288
315
|
|
|
289
316
|
await watcher.start();
|
|
290
317
|
} catch (err) {
|
|
318
|
+
cleanupShutdownHandlers();
|
|
291
319
|
if (jsonMode) {
|
|
292
320
|
console.log = originalLog;
|
|
293
321
|
console.log(JSON.stringify({ error: err.message }, null, 2));
|
package/package.json
CHANGED
package/src/agents/base.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const pty = require('node-pty');
|
|
2
|
+
const process = require('node:process');
|
|
2
3
|
const logger = require('../logger.js');
|
|
3
4
|
|
|
4
5
|
class BaseAgent {
|
|
@@ -24,6 +25,22 @@ class BaseAgent {
|
|
|
24
25
|
this._onDataCallback = null;
|
|
25
26
|
this._commandInFlight = false;
|
|
26
27
|
this._disposables = [];
|
|
28
|
+
this._stopRequested = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_createStopError() {
|
|
32
|
+
return new Error('Agent stopping');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isStopping() {
|
|
36
|
+
return this._stopRequested;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
requestStop() {
|
|
40
|
+
this._stopRequested = true;
|
|
41
|
+
this.isRefreshing = false;
|
|
42
|
+
this._commandInFlight = false;
|
|
43
|
+
this._onDataCallback = null;
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
getStatus() {
|
|
@@ -39,6 +56,11 @@ class BaseAgent {
|
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
async refresh() {
|
|
59
|
+
if (this.isStopping()) {
|
|
60
|
+
logger.agent(this.name, 'Skipping refresh while stopping');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
if (this.isRefreshing) {
|
|
43
65
|
logger.agent(this.name, 'Already refreshing, skipping...');
|
|
44
66
|
return;
|
|
@@ -68,6 +90,10 @@ class BaseAgent {
|
|
|
68
90
|
}
|
|
69
91
|
}
|
|
70
92
|
|
|
93
|
+
if (this.isStopping()) {
|
|
94
|
+
throw this._createStopError();
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
const output = await this.runCommand();
|
|
72
98
|
logger.agent(this.name, 'Got output, length:', logger.dim(`${output.length} chars`));
|
|
73
99
|
|
|
@@ -108,6 +134,10 @@ class BaseAgent {
|
|
|
108
134
|
if (hasData) { this.usage = parsed; this.lastUpdated = new Date().toISOString(); } else if (this.usage) { logger.agent(this.name, 'Parse returned no data, preserving last known usage'); this.error = 'Failed to parse — using cached data'; } else { this.usage = parsed; this.lastUpdated = new Date().toISOString(); }
|
|
109
135
|
logger.agent(this.name, 'Parsed usage:', logger.json(this.usage));
|
|
110
136
|
} catch (err) {
|
|
137
|
+
if (this.isStopping() && err.message === 'Agent stopping') {
|
|
138
|
+
logger.agent(this.name, 'Refresh cancelled during shutdown');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
111
141
|
logger.error(`[${this.name}] Error during refresh:`, err.message);
|
|
112
142
|
this.error = err.message;
|
|
113
143
|
} finally {
|
|
@@ -117,6 +147,10 @@ class BaseAgent {
|
|
|
117
147
|
}
|
|
118
148
|
|
|
119
149
|
async runCommand() {
|
|
150
|
+
if (this.isStopping()) {
|
|
151
|
+
throw this._createStopError();
|
|
152
|
+
}
|
|
153
|
+
|
|
120
154
|
if (this.freshProcess) {
|
|
121
155
|
return this._runCommandFresh();
|
|
122
156
|
}
|
|
@@ -130,6 +164,10 @@ class BaseAgent {
|
|
|
130
164
|
}
|
|
131
165
|
|
|
132
166
|
async spawnProcess() {
|
|
167
|
+
if (this.isStopping()) {
|
|
168
|
+
throw this._createStopError();
|
|
169
|
+
}
|
|
170
|
+
|
|
133
171
|
return new Promise((resolve, reject) => {
|
|
134
172
|
let trustHandled = false;
|
|
135
173
|
let spawnOutput = '';
|
|
@@ -145,6 +183,9 @@ class BaseAgent {
|
|
|
145
183
|
cwd: '/tmp',
|
|
146
184
|
env,
|
|
147
185
|
});
|
|
186
|
+
if (typeof this.shell.pid === 'number') {
|
|
187
|
+
logger.agent(this.name, 'Persistent process pid:', logger.dim(String(this.shell.pid)));
|
|
188
|
+
}
|
|
148
189
|
} catch (spawnErr) {
|
|
149
190
|
logger.error(`[${this.name}] Failed to spawn:`, spawnErr.message);
|
|
150
191
|
reject(new Error(`Failed to spawn ${this.command}: ${spawnErr.message}`));
|
|
@@ -152,6 +193,10 @@ class BaseAgent {
|
|
|
152
193
|
}
|
|
153
194
|
|
|
154
195
|
const timer = setTimeout(() => {
|
|
196
|
+
if (this.isStopping()) {
|
|
197
|
+
reject(this._createStopError());
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
155
200
|
logger.error(`[${this.name}] Spawn timeout after ${this.getTimeout()}ms`);
|
|
156
201
|
this.killProcess();
|
|
157
202
|
reject(new Error('Timeout waiting for process to become ready'));
|
|
@@ -159,6 +204,15 @@ class BaseAgent {
|
|
|
159
204
|
|
|
160
205
|
let trustCooldownUntil = 0;
|
|
161
206
|
const spawnDataHandler = this.shell.onData((data) => {
|
|
207
|
+
if (this.isStopping()) {
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
spawnDataHandler.dispose();
|
|
210
|
+
spawnExitHandler.dispose();
|
|
211
|
+
this.killProcess();
|
|
212
|
+
reject(this._createStopError());
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
162
216
|
spawnOutput += data;
|
|
163
217
|
|
|
164
218
|
// Handle trust/update prompts during spawn (only once each)
|
|
@@ -202,12 +256,20 @@ class BaseAgent {
|
|
|
202
256
|
spawnExitHandler.dispose();
|
|
203
257
|
this.shell = null;
|
|
204
258
|
this.processReady = false;
|
|
259
|
+
if (this.isStopping()) {
|
|
260
|
+
reject(this._createStopError());
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
205
263
|
reject(new Error(`Process exited during spawn with code ${exitCode}`));
|
|
206
264
|
});
|
|
207
265
|
});
|
|
208
266
|
}
|
|
209
267
|
|
|
210
268
|
async sendCommandAndWait() {
|
|
269
|
+
if (this.isStopping()) {
|
|
270
|
+
throw this._createStopError();
|
|
271
|
+
}
|
|
272
|
+
|
|
211
273
|
return new Promise((resolve, reject) => {
|
|
212
274
|
const existingOutput = this.output;
|
|
213
275
|
const existingAuthState = this.detectAuthenticationState ? this.detectAuthenticationState(existingOutput) : null;
|
|
@@ -228,6 +290,12 @@ class BaseAgent {
|
|
|
228
290
|
};
|
|
229
291
|
|
|
230
292
|
const timer = setTimeout(() => {
|
|
293
|
+
if (this.isStopping()) {
|
|
294
|
+
this._commandInFlight = false;
|
|
295
|
+
this._onDataCallback = null;
|
|
296
|
+
reject(this._createStopError());
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
231
299
|
logger.agent(this.name, 'Command timeout after', logger.dim(`${this.getTimeout()}ms`), ', output length:', logger.dim(`${this.output.length}`));
|
|
232
300
|
if (this.output.length > 0) {
|
|
233
301
|
logger.agent(this.name, 'Partial output:', logger.dim(this.stripAnsi(this.output).substring(0, 500)));
|
|
@@ -245,6 +313,13 @@ class BaseAgent {
|
|
|
245
313
|
}, this.getTimeout());
|
|
246
314
|
|
|
247
315
|
this._onDataCallback = () => {
|
|
316
|
+
if (this.isStopping()) {
|
|
317
|
+
this._commandInFlight = false;
|
|
318
|
+
this._onDataCallback = null;
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
reject(this._createStopError());
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
248
323
|
if (this.hasCompleteOutput(this.output)) {
|
|
249
324
|
logger.agent(this.name, 'Complete output detected');
|
|
250
325
|
// Small delay to capture any remaining output
|
|
@@ -298,15 +373,27 @@ class BaseAgent {
|
|
|
298
373
|
_handleAdditionalPrompts(_s, _d, _o) { } // Hook for subclasses; killProcess: see below
|
|
299
374
|
killProcess() { // Terminates the persistent PTY process
|
|
300
375
|
if (this.shell) {
|
|
301
|
-
|
|
376
|
+
const pidText = typeof this.shell.pid === 'number' ? ` (pid ${this.shell.pid})` : '';
|
|
377
|
+
logger.agent(this.name, `Killing persistent process${pidText}`);
|
|
378
|
+
const shell = this.shell;
|
|
302
379
|
for (const d of this._disposables) { d.dispose(); }
|
|
303
380
|
this._disposables = []; this._onDataCallback = null; this.processReady = false;
|
|
304
|
-
try {
|
|
381
|
+
try {
|
|
382
|
+
// PTY-backed CLIs may spawn descendants; kill the whole process group when available.
|
|
383
|
+
if (typeof shell.pid === 'number' && shell.pid > 0) {
|
|
384
|
+
try { process.kill(-shell.pid, 'SIGTERM'); } catch (_e) { /* Group may not exist */ }
|
|
385
|
+
}
|
|
386
|
+
shell.kill();
|
|
387
|
+
} catch (_e) { /* Process may already be dead */ }
|
|
305
388
|
this.shell = null;
|
|
306
389
|
}
|
|
307
390
|
}
|
|
308
391
|
|
|
309
392
|
async _runCommandFresh() {
|
|
393
|
+
if (this.isStopping()) {
|
|
394
|
+
throw this._createStopError();
|
|
395
|
+
}
|
|
396
|
+
|
|
310
397
|
return new Promise((resolve, reject) => {
|
|
311
398
|
const timeout = this.getTimeout();
|
|
312
399
|
let output = '';
|
|
@@ -334,6 +421,12 @@ class BaseAgent {
|
|
|
334
421
|
|
|
335
422
|
const timer = setTimeout(() => {
|
|
336
423
|
if (!completed) {
|
|
424
|
+
if (this.isStopping()) {
|
|
425
|
+
completed = true;
|
|
426
|
+
shell.kill();
|
|
427
|
+
reject(this._createStopError());
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
337
430
|
completed = true;
|
|
338
431
|
logger.agent(this.name, 'Timeout after', logger.dim(`${timeout}ms`), ', output length:', logger.dim(`${output.length}`));
|
|
339
432
|
if (output.length > 0) {
|
|
@@ -352,6 +445,16 @@ class BaseAgent {
|
|
|
352
445
|
|
|
353
446
|
let trustCooldownUntil = 0;
|
|
354
447
|
shell.onData((data) => {
|
|
448
|
+
if (this.isStopping()) {
|
|
449
|
+
if (!completed) {
|
|
450
|
+
completed = true;
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
shell.kill();
|
|
453
|
+
reject(this._createStopError());
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
355
458
|
output += data;
|
|
356
459
|
|
|
357
460
|
if (output.length <= data.length) {
|
|
@@ -395,6 +498,10 @@ class BaseAgent {
|
|
|
395
498
|
if (!completed) {
|
|
396
499
|
completed = true;
|
|
397
500
|
clearTimeout(timer);
|
|
501
|
+
if (this.isStopping()) {
|
|
502
|
+
reject(this._createStopError());
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
398
505
|
logger.agent(this.name, 'Process exited with code', logger.dim(exitCode), ', output length:', logger.dim(`${output.length}`));
|
|
399
506
|
if (output) {
|
|
400
507
|
resolve(output);
|
|
@@ -414,6 +521,7 @@ class BaseAgent {
|
|
|
414
521
|
|
|
415
522
|
/** Lightweight keepalive method to prevent session expiration. Subclasses can override. @returns {Promise<boolean>} True if keepalive succeeded */
|
|
416
523
|
async keepalive() {
|
|
524
|
+
if (this.isStopping()) { return false; }
|
|
417
525
|
if (this.freshProcess) { console.log(`[${this.name}] Keepalive skipped (fresh process mode)`); return true; }
|
|
418
526
|
if (!this.shell || !this.processReady) { console.log(`[${this.name}] Keepalive: spawning process...`); await this.spawnProcess(); }
|
|
419
527
|
if (this.shell) { console.log(`[${this.name}] Keepalive: sending ping...`); this.shell.write('\x1b'); return true; }
|
package/src/agents/claude.js
CHANGED
|
@@ -204,43 +204,25 @@ class ClaudeAgent extends BaseAgent {
|
|
|
204
204
|
const clean = this.stripAnsi(output);
|
|
205
205
|
// Detect error responses (rate limiting, session errors) — treat as complete
|
|
206
206
|
if (/rate.?limited|rate_limit_error|Failed to load usage|session.?expired|session.?error|invalid.?session|authentication.?error|auth.?failed|Unable to (?:load|fetch)|Error loading|could not (?:load|fetch)|not authenticated|login required|sign.?in required/i.test(clean)) return true;
|
|
207
|
-
|
|
208
|
-
const hasSession = clean.includes('Current session');
|
|
209
|
-
const hasWeekly = clean.includes('Current week');
|
|
210
|
-
const hasPercentUsed = clean.includes('% used');
|
|
211
|
-
if (!hasSession || !hasWeekly || !hasPercentUsed) return false;
|
|
212
|
-
|
|
213
|
-
// If "all models" section exists, wait for "Sonnet only" section to render
|
|
214
|
-
const hasAllModels = /Current\s+week\s*\(?\s*all\s+models/i.test(clean);
|
|
215
|
-
if (hasAllModels) {
|
|
216
|
-
const hasSonnetOnly = /Current\s+week\s*\(?\s*Sonnet\s+only/i.test(clean);
|
|
217
|
-
if (!hasSonnetOnly) {
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
// Both sections exist, check we have at least one timezone per section
|
|
221
|
-
const allModelsIdx = clean.search(/Current\s+week\s*\(?\s*all\s+models/i);
|
|
222
|
-
const sonnetOnlyIdx = clean.search(/Current\s+week\s*\(?\s*Sonnet\s+only/i);
|
|
223
|
-
const allModelsSection = clean.substring(allModelsIdx, sonnetOnlyIdx);
|
|
224
|
-
const sonnetSection = clean.substring(sonnetOnlyIdx);
|
|
225
|
-
|
|
226
|
-
const allModelsHasTimezone = /\([A-Za-z]+\/[A-Za-z_]+\)/.test(allModelsSection);
|
|
227
|
-
const sonnetHasTimezone = /\([A-Za-z]+\/[A-Za-z_]+\)/.test(sonnetSection);
|
|
207
|
+
const parsed = parsePtyOutput(clean);
|
|
228
208
|
|
|
229
|
-
|
|
230
|
-
|
|
209
|
+
const hasSessionData = parsed.session && typeof parsed.session.percent === 'number';
|
|
210
|
+
const hasLegacyWeekly = parsed.weekly && typeof parsed.weekly.percent === 'number';
|
|
211
|
+
const hasAllModelsWeekly = parsed.weeklyAll && typeof parsed.weeklyAll.percent === 'number';
|
|
231
212
|
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
return hasTimezone;
|
|
213
|
+
// Newer Claude builds often emit usable session/weekly data before the UI fully settles.
|
|
214
|
+
// If we can already parse session data plus either weekly format, treat the response as complete.
|
|
215
|
+
return hasSessionData && (hasLegacyWeekly || hasAllModelsWeekly);
|
|
236
216
|
}
|
|
237
217
|
|
|
238
218
|
sendCommands(shell, _output) {
|
|
239
219
|
logger.agent(this.name, 'Sending /usage command...');
|
|
240
|
-
//
|
|
220
|
+
// Claude keeps slash-command suggestions open for /usage on newer builds.
|
|
221
|
+
// Confirm the command selection, then submit the actual command execution.
|
|
241
222
|
setTimeout(() => shell.write('\x1b'), 50);
|
|
242
223
|
setTimeout(() => shell.write('/usage'), 600);
|
|
243
224
|
setTimeout(() => shell.write('\r'), 1000);
|
|
225
|
+
setTimeout(() => shell.write('\r'), 1400);
|
|
244
226
|
}
|
|
245
227
|
|
|
246
228
|
// After getting /usage output, dismiss dialog so next refresh starts with clean prompt
|
package/src/agents/codex.js
CHANGED
|
@@ -156,6 +156,9 @@ class CodexAgent extends BaseAgent {
|
|
|
156
156
|
cwd: '/tmp',
|
|
157
157
|
env: { ...process.env, TERM: 'xterm-256color' },
|
|
158
158
|
});
|
|
159
|
+
if (typeof this.shell.pid === 'number') {
|
|
160
|
+
logger.agent(this.name, 'Persistent process pid:', logger.dim(String(this.shell.pid)));
|
|
161
|
+
}
|
|
159
162
|
} catch (spawnErr) {
|
|
160
163
|
logger.error(`[${this.name}] Failed to spawn:`, spawnErr.message);
|
|
161
164
|
reject(new Error(`Failed to spawn ${this.command}: ${spawnErr.message}`));
|
|
@@ -87,8 +87,8 @@ ${metricExtractors}
|
|
|
87
87
|
if (modelContainer) {
|
|
88
88
|
const resetWrapper = modelContainer.querySelector('.reset-info-wrapper');
|
|
89
89
|
if (resetWrapper) {
|
|
90
|
-
if (
|
|
91
|
-
// Hide
|
|
90
|
+
if (!resetsIn) {
|
|
91
|
+
// Hide reset info only when the backend provides no reset time
|
|
92
92
|
resetWrapper.style.display = 'none';
|
|
93
93
|
} else {
|
|
94
94
|
resetWrapper.style.display = '';
|