agent-tank 0.9.9 โ 0.9.11
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/CHANGELOG.md +157 -0
- package/README.md +84 -9
- package/bin/agent-tank.js +26 -8
- package/favicon.svg +7 -0
- package/package.json +3 -1
- package/src/activity-monitor.js +6 -1
- package/src/agent-config.js +105 -0
- package/src/agents/agy.js +48 -10
- package/src/agents/api-response-parser.js +17 -17
- package/src/agents/base.js +29 -14
- package/src/agents/claude.js +149 -22
- package/src/agents/codex-rpc-helpers.js +166 -34
- package/src/agents/codex.js +42 -9
- package/src/agents/oauth-helper.js +12 -4
- package/src/agents/pty-output-parser.js +28 -10
- package/src/auto-refresh-manager.js +6 -1
- package/src/client-auto-refresh-helpers.js +13 -13
- package/src/client-auto-refresh.js +9 -7
- package/src/client-script.js +1 -1
- package/src/index.js +49 -22
- package/src/pace-attachment.js +2 -2
- package/src/snapshot-metrics.js +1 -1
- package/src/status-page.js +11 -6
- package/src/usage-formatters.js +14 -12
package/src/agents/codex.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- Codex JSON-RPC and PTY integrations share one provider implementation. */
|
|
2
|
+
|
|
1
3
|
const { BaseAgent } = require('./base.js');
|
|
2
4
|
const logger = require('../logger.js');
|
|
3
5
|
const { JsonRpcClient } = require('../json-rpc-client.js');
|
|
@@ -13,16 +15,24 @@ const {
|
|
|
13
15
|
extractMetadataFromOutput,
|
|
14
16
|
} = require('./codex-pty-helpers.js');
|
|
15
17
|
const { pingKeepalive } = require('./keepalive-helper.js');
|
|
18
|
+
const { version: packageVersion } = require('../../package.json');
|
|
16
19
|
|
|
17
20
|
class CodexAgent extends BaseAgent {
|
|
18
|
-
constructor() {
|
|
21
|
+
constructor(options = {}) {
|
|
19
22
|
super('codex', 'codex');
|
|
23
|
+
this.configPath = options.configPath || null;
|
|
20
24
|
this._rpcClient = null;
|
|
21
25
|
this._rpcSupported = null; // null = unknown, true/false = tested
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
getTimeout() { return 25000; }
|
|
25
29
|
|
|
30
|
+
getEnv() {
|
|
31
|
+
const env = { ...process.env, TERM: 'xterm-256color' };
|
|
32
|
+
if (this.configPath) env.CODEX_HOME = this.configPath;
|
|
33
|
+
return env;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
/**
|
|
27
37
|
* Override runCommand to attempt JSON-RPC first, then fall back to PTY
|
|
28
38
|
*/
|
|
@@ -55,8 +65,9 @@ class CodexAgent extends BaseAgent {
|
|
|
55
65
|
this._rpcClient = null;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
this._rpcClient = new JsonRpcClient('codex', ['
|
|
68
|
+
this._rpcClient = new JsonRpcClient('codex', ['app-server'], {
|
|
59
69
|
cwd: '/tmp',
|
|
70
|
+
env: this.getEnv(),
|
|
60
71
|
timeout: this.getTimeout(),
|
|
61
72
|
});
|
|
62
73
|
|
|
@@ -64,6 +75,15 @@ class CodexAgent extends BaseAgent {
|
|
|
64
75
|
await this._rpcClient.start();
|
|
65
76
|
console.log(`[${this.name}] JSON-RPC server started`);
|
|
66
77
|
|
|
78
|
+
await this._rpcClient.call('initialize', {
|
|
79
|
+
clientInfo: {
|
|
80
|
+
name: 'agent_tank',
|
|
81
|
+
title: 'Agent Tank',
|
|
82
|
+
version: packageVersion,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
this._rpcClient.notify('initialized', {});
|
|
86
|
+
|
|
67
87
|
const rateLimits = await this._rpcClient.call('account/rateLimits/read', {});
|
|
68
88
|
console.log(`[${this.name}] Received rate limits via JSON-RPC:`, JSON.stringify(rateLimits));
|
|
69
89
|
|
|
@@ -112,7 +132,14 @@ class CodexAgent extends BaseAgent {
|
|
|
112
132
|
return output.includes('? for shortcuts') || output.includes('To get started') ||
|
|
113
133
|
output.includes('% left') || /โบ\s*\S/.test(this.stripAnsi(output));
|
|
114
134
|
}
|
|
115
|
-
sendCommands(shell, _output) {
|
|
135
|
+
sendCommands(shell, _output) {
|
|
136
|
+
logger.agent(this.name, 'Sending /status command...');
|
|
137
|
+
// Codex 0.158 keeps the slash-command picker open after the first Enter.
|
|
138
|
+
// Confirm the picker entry, then submit the selected command.
|
|
139
|
+
setTimeout(() => { if (shell) shell.write('/status'); }, 100);
|
|
140
|
+
setTimeout(() => { if (shell) shell.write('\r'); }, 500);
|
|
141
|
+
setTimeout(() => { if (shell) shell.write('\r'); }, 900);
|
|
142
|
+
}
|
|
116
143
|
|
|
117
144
|
_handleAdditionalPrompts(shell, _data, output) {
|
|
118
145
|
if (!shell) return;
|
|
@@ -154,7 +181,7 @@ class CodexAgent extends BaseAgent {
|
|
|
154
181
|
cols: 120,
|
|
155
182
|
rows: 40,
|
|
156
183
|
cwd: '/tmp',
|
|
157
|
-
env:
|
|
184
|
+
env: this.getEnv(),
|
|
158
185
|
});
|
|
159
186
|
if (typeof this.shell.pid === 'number') {
|
|
160
187
|
logger.agent(this.name, 'Persistent process pid:', logger.dim(String(this.shell.pid)));
|
|
@@ -244,8 +271,7 @@ class CodexAgent extends BaseAgent {
|
|
|
244
271
|
if (!this.hasCompleteOutput(this.output) && this.shell) {
|
|
245
272
|
logger.agent(this.name, 'Retrying /status...');
|
|
246
273
|
this.output = '';
|
|
247
|
-
this.shell
|
|
248
|
-
setTimeout(() => { if (this.shell) this.shell.write('\r'); }, 200);
|
|
274
|
+
this.sendCommands(this.shell, '');
|
|
249
275
|
}
|
|
250
276
|
}, 5000);
|
|
251
277
|
|
|
@@ -264,8 +290,15 @@ class CodexAgent extends BaseAgent {
|
|
|
264
290
|
}
|
|
265
291
|
|
|
266
292
|
hasCompleteOutput(output) {
|
|
267
|
-
|
|
268
|
-
return
|
|
293
|
+
const clean = this.stripAnsi(output);
|
|
294
|
+
if (clean.includes('5h limit') && clean.includes('Weekly limit')) return true;
|
|
295
|
+
|
|
296
|
+
const hasWeekly = /Weekly limit:\s*\[.*?\]\s*\d+%\s*left\s*\(resets\s*[^)]+\)/i.test(clean);
|
|
297
|
+
if (!hasWeekly) return false;
|
|
298
|
+
|
|
299
|
+
// Pro accounts can expose only the weekly window (secondary is null in the
|
|
300
|
+
// equivalent JSON-RPC response), so waiting for a 5h row always times out.
|
|
301
|
+
return true;
|
|
269
302
|
}
|
|
270
303
|
|
|
271
304
|
parseResetTime(resetStr) {
|
|
@@ -372,7 +405,7 @@ class CodexAgent extends BaseAgent {
|
|
|
372
405
|
name: this.name,
|
|
373
406
|
command: this.command,
|
|
374
407
|
args: this.args,
|
|
375
|
-
env:
|
|
408
|
+
env: this.getEnv(),
|
|
376
409
|
termName: 'xterm-256color',
|
|
377
410
|
isReady: (output) => this.isReadyForStatus(output),
|
|
378
411
|
sendCommand: (shell) => setTimeout(() => shell.write('/status\r'), 100),
|
|
@@ -91,7 +91,7 @@ function readSecretToolCredentials() {
|
|
|
91
91
|
* Read OAuth credentials from all available sources in priority order.
|
|
92
92
|
* @returns {{ accessToken: string, refreshToken?: string, expiresAt?: number, source: string }|null}
|
|
93
93
|
*/
|
|
94
|
-
function readCredentials() {
|
|
94
|
+
function readCredentials(configPath = null) {
|
|
95
95
|
// 1. Environment variable (no refresh possible)
|
|
96
96
|
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
97
97
|
return { accessToken: process.env.CLAUDE_CODE_OAUTH_TOKEN, source: 'env' };
|
|
@@ -99,10 +99,16 @@ function readCredentials() {
|
|
|
99
99
|
|
|
100
100
|
// 2. Credentials file
|
|
101
101
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
102
|
-
const credentialsPath =
|
|
102
|
+
const credentialsPath = configPath
|
|
103
|
+
? path.join(configPath, '.credentials.json')
|
|
104
|
+
: path.join(homeDir, '.claude', '.credentials.json');
|
|
103
105
|
const fileCreds = readCredentialsFile(credentialsPath);
|
|
104
106
|
if (fileCreds) return { ...fileCreds, source: 'credentials_file' };
|
|
105
107
|
|
|
108
|
+
// Keychain helpers only know the default Claude account. Falling through here
|
|
109
|
+
// would silently read the wrong account for a custom CLAUDE_CONFIG_DIR.
|
|
110
|
+
if (configPath) return null;
|
|
111
|
+
|
|
106
112
|
// 3. macOS Keychain
|
|
107
113
|
const keychainCreds = readKeychainCredentials();
|
|
108
114
|
if (keychainCreds) return { ...keychainCreds, source: 'keychain' };
|
|
@@ -185,9 +191,11 @@ function refreshOAuthToken(refreshToken) {
|
|
|
185
191
|
* Persist refreshed tokens back to the credentials file.
|
|
186
192
|
* @param {{ accessToken: string, refreshToken: string, expiresAt: number|null }} tokens
|
|
187
193
|
*/
|
|
188
|
-
function persistRefreshedTokens(tokens) {
|
|
194
|
+
function persistRefreshedTokens(tokens, configPath = null) {
|
|
189
195
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
190
|
-
const credentialsPath =
|
|
196
|
+
const credentialsPath = configPath
|
|
197
|
+
? path.join(configPath, '.credentials.json')
|
|
198
|
+
: path.join(homeDir, '.claude', '.credentials.json');
|
|
191
199
|
const raw = fs.readFileSync(credentialsPath, 'utf8');
|
|
192
200
|
const credentials = JSON.parse(raw);
|
|
193
201
|
credentials.claudeAiOauth = {
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
const { calculatePace } = require('../pace-evaluator.js');
|
|
7
7
|
const { CYCLE_DURATIONS } = require('../usage-formatters.js');
|
|
8
8
|
|
|
9
|
+
// Header of the Fable allowance section: "Current week (Fable)" or a standalone
|
|
10
|
+
// "Fable allowance" label. PTY redraws often drop letters ("Curnt week(Fable)"),
|
|
11
|
+
// so the "Current" word is matched loosely.
|
|
12
|
+
const FABLE_SECTION_START = '(?:C\\w*\\s*week\\s*\\(?\\s*Fable[^)%\\n]*\\)?|Fable\\s*allowance)';
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Convert 12-hour time to 24-hour
|
|
11
16
|
* @param {string} hourRaw - Hour value as string
|
|
@@ -248,27 +253,39 @@ function addPaceData(sectionData, cycleType) {
|
|
|
248
253
|
* @returns {Object} Parsed usage object
|
|
249
254
|
*/
|
|
250
255
|
function parsePtyOutput(clean) {
|
|
251
|
-
const usage = { session: null, weeklyAll: null,
|
|
256
|
+
const usage = { session: null, weeklyAll: null, weeklyFable: null };
|
|
252
257
|
|
|
253
|
-
// Parse session
|
|
254
|
-
|
|
258
|
+
// Parse session. The "Current session" label is frequently corrupted while
|
|
259
|
+
// Claude's /usage dialog is still rendering (e.g. "Curretsession"), even
|
|
260
|
+
// though the bar, "N% used", and reset time are intact. When the labeled
|
|
261
|
+
// extraction fails, fall back to the region before the weekly section โ the
|
|
262
|
+
// session block is the only "N% used" that appears there.
|
|
263
|
+
let sessionData = parseUsageSection(extractSection(clean, 'Current\\s*session', 'Current\\s*week'));
|
|
264
|
+
if (!sessionData) {
|
|
265
|
+
const beforeWeekly = clean.split(/Current\s*week/i);
|
|
266
|
+
// length > 1 confirms the weekly section exists, i.e. this is real /usage
|
|
267
|
+
// output and beforeWeekly[0] is the session region (not unrelated text).
|
|
268
|
+
if (beforeWeekly.length > 1) {
|
|
269
|
+
sessionData = parseUsageSection(beforeWeekly[0]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
255
272
|
if (sessionData) {
|
|
256
273
|
usage.session = { label: 'Current session', ...sessionData };
|
|
257
274
|
addPaceData(usage.session, 'session');
|
|
258
275
|
}
|
|
259
276
|
|
|
260
277
|
// Parse weekly (all models)
|
|
261
|
-
const weeklyAllData = parseUsageSection(extractSection(clean, 'Current\\s*week\\s*\\(?\\s*all\\s*models\\s*\\)?',
|
|
278
|
+
const weeklyAllData = parseUsageSection(extractSection(clean, 'Current\\s*week\\s*\\(?\\s*all\\s*models\\s*\\)?', `Extra\\s*usage|esc\\s*to\\s*cancel|${FABLE_SECTION_START}`));
|
|
262
279
|
if (weeklyAllData) {
|
|
263
280
|
usage.weeklyAll = { label: 'Current week (all models)', ...weeklyAllData };
|
|
264
281
|
addPaceData(usage.weeklyAll, 'weekly');
|
|
265
282
|
}
|
|
266
283
|
|
|
267
|
-
// Parse weekly (
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
270
|
-
usage.
|
|
271
|
-
addPaceData(usage.
|
|
284
|
+
// Parse weekly Fable allowance (tracked separately from the all-models cap)
|
|
285
|
+
const weeklyFableData = parseUsageSection(extractSection(clean, FABLE_SECTION_START, 'Extra\\s*usage|Usage\\s*credits|esc\\s*to\\s*cancel|Current\\s*week\\s*\\('));
|
|
286
|
+
if (weeklyFableData) {
|
|
287
|
+
usage.weeklyFable = { label: 'Current week (Fable)', ...weeklyFableData };
|
|
288
|
+
addPaceData(usage.weeklyFable, 'weekly');
|
|
272
289
|
}
|
|
273
290
|
|
|
274
291
|
// Parse extra usage section
|
|
@@ -282,7 +299,7 @@ function parsePtyOutput(clean) {
|
|
|
282
299
|
}
|
|
283
300
|
|
|
284
301
|
// Legacy format fallback
|
|
285
|
-
if (!usage.weeklyAll && !usage.
|
|
302
|
+
if (!usage.weeklyAll && !usage.weeklyFable) {
|
|
286
303
|
const legacyWeekly = parseLegacyWeekly(clean);
|
|
287
304
|
if (legacyWeekly) {
|
|
288
305
|
usage.weekly = legacyWeekly;
|
|
@@ -294,6 +311,7 @@ function parsePtyOutput(clean) {
|
|
|
294
311
|
}
|
|
295
312
|
|
|
296
313
|
module.exports = {
|
|
314
|
+
FABLE_SECTION_START,
|
|
297
315
|
parsePtyOutput,
|
|
298
316
|
parseResetTime,
|
|
299
317
|
formatDuration,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { ActivityMonitor } = require('./activity-monitor.js');
|
|
1
|
+
const { ActivityMonitor, getAgentLogDirectories } = require('./activity-monitor.js');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Manages auto-refresh behavior for AgentTank.
|
|
@@ -117,6 +117,10 @@ class AutoRefreshManager {
|
|
|
117
117
|
*/
|
|
118
118
|
_startActivityBasedRefresh() {
|
|
119
119
|
const agentNames = Array.from(this.agents.keys());
|
|
120
|
+
const logDirectories = {};
|
|
121
|
+
for (const [id, agent] of this.agents) {
|
|
122
|
+
logDirectories[id] = getAgentLogDirectories(agent.provider || agent.name, agent.configPath);
|
|
123
|
+
}
|
|
120
124
|
const debounceMs = this.config.activityDebounce;
|
|
121
125
|
const intervalSeconds = this.config.interval;
|
|
122
126
|
|
|
@@ -125,6 +129,7 @@ class AutoRefreshManager {
|
|
|
125
129
|
this.activityMonitor = new ActivityMonitor({
|
|
126
130
|
debounceInterval: debounceMs,
|
|
127
131
|
agents: agentNames,
|
|
132
|
+
logDirectories,
|
|
128
133
|
onActivity: (event) => {
|
|
129
134
|
this._handleActivityDetected(event);
|
|
130
135
|
},
|
|
@@ -49,20 +49,20 @@ const timeHelpers = `
|
|
|
49
49
|
|
|
50
50
|
const metricExtractors = `
|
|
51
51
|
// Extract metrics from Claude usage data
|
|
52
|
-
function extractClaudeMetrics(usage) {
|
|
52
|
+
function extractClaudeMetrics(usage, agentId = 'claude') {
|
|
53
53
|
const metrics = [];
|
|
54
54
|
const sections = [
|
|
55
55
|
{ data: usage.session, label: 'Session', cycle: 'session' },
|
|
56
56
|
{ data: usage.weeklyAll, label: 'Weekly (all)', cycle: 'weekly' },
|
|
57
|
-
{ data: usage.
|
|
57
|
+
{ data: usage.weeklyFable, label: 'Weekly (Fable)', cycle: 'weekly' },
|
|
58
58
|
{ data: usage.weekly, label: 'Weekly', cycle: 'weekly' }
|
|
59
59
|
];
|
|
60
60
|
for (const { data, label, cycle } of sections) {
|
|
61
61
|
if (data) {
|
|
62
|
-
const metricId =
|
|
62
|
+
const metricId = \`\${agentId}-\${label.toLowerCase().replace(/[^a-z0-9]/g, '-')}\`;
|
|
63
63
|
metrics.push({
|
|
64
64
|
metricId,
|
|
65
|
-
agent:
|
|
65
|
+
agent: agentId,
|
|
66
66
|
label,
|
|
67
67
|
percent: data.percent ?? 0,
|
|
68
68
|
resetsIn: data.resetsIn || '',
|
|
@@ -75,20 +75,20 @@ const metricExtractors = `
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Extract metrics from Antigravity usage data
|
|
78
|
-
function extractAgyMetrics(usage) {
|
|
78
|
+
function extractAgyMetrics(usage, agentId = 'agy') {
|
|
79
79
|
const metrics = [];
|
|
80
80
|
if (usage.models && usage.models.length > 0) {
|
|
81
81
|
for (const model of usage.models) {
|
|
82
82
|
const modelName = model.model;
|
|
83
|
-
const metricId =
|
|
83
|
+
const metricId = \`\${agentId}-\${modelName.toLowerCase().replace(/[^a-z0-9]/g, '-')}\`;
|
|
84
84
|
metrics.push({
|
|
85
85
|
metricId,
|
|
86
|
-
agent:
|
|
86
|
+
agent: agentId,
|
|
87
87
|
label: modelName,
|
|
88
88
|
percent: model.percentUsed ?? 0,
|
|
89
89
|
resetsIn: model.resetsIn || '',
|
|
90
90
|
resetsAt: model.resetsAt || '',
|
|
91
|
-
cycle: 'sessionAgy'
|
|
91
|
+
cycle: model.cycle || 'sessionAgy'
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -96,7 +96,7 @@ const metricExtractors = `
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Extract metrics from Codex usage data
|
|
99
|
-
function extractCodexMetrics(usage) {
|
|
99
|
+
function extractCodexMetrics(usage, agentId = 'codex') {
|
|
100
100
|
const metrics = [];
|
|
101
101
|
const models = [];
|
|
102
102
|
if (usage.fiveHour || usage.weekly) {
|
|
@@ -109,8 +109,8 @@ const metricExtractors = `
|
|
|
109
109
|
const modelSlug = ml.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
110
110
|
if (ml.fiveHour) {
|
|
111
111
|
metrics.push({
|
|
112
|
-
metricId:
|
|
113
|
-
agent:
|
|
112
|
+
metricId: \`\${agentId}-\${modelSlug}-5h\`,
|
|
113
|
+
agent: agentId,
|
|
114
114
|
label: '5h limit',
|
|
115
115
|
modelName: ml.name,
|
|
116
116
|
percent: ml.fiveHour.percentUsed ?? 0,
|
|
@@ -121,8 +121,8 @@ const metricExtractors = `
|
|
|
121
121
|
}
|
|
122
122
|
if (ml.weekly) {
|
|
123
123
|
metrics.push({
|
|
124
|
-
metricId:
|
|
125
|
-
agent:
|
|
124
|
+
metricId: \`\${agentId}-\${modelSlug}-weekly\`,
|
|
125
|
+
agent: agentId,
|
|
126
126
|
label: 'Weekly',
|
|
127
127
|
modelName: ml.name,
|
|
128
128
|
percent: ml.weekly.percentUsed ?? 0,
|
|
@@ -139,13 +139,14 @@ ${metricExtractors}
|
|
|
139
139
|
for (const [agentName, agentData] of Object.entries(status)) {
|
|
140
140
|
if (!agentData.usage) continue;
|
|
141
141
|
|
|
142
|
+
const provider = agentData.provider || agentData.name || agentName;
|
|
142
143
|
let metrics = [];
|
|
143
|
-
if (
|
|
144
|
-
metrics = extractClaudeMetrics(agentData.usage);
|
|
145
|
-
} else if (
|
|
146
|
-
metrics = extractAgyMetrics(agentData.usage);
|
|
147
|
-
} else if (
|
|
148
|
-
metrics = extractCodexMetrics(agentData.usage);
|
|
144
|
+
if (provider === 'claude') {
|
|
145
|
+
metrics = extractClaudeMetrics(agentData.usage, agentName);
|
|
146
|
+
} else if (provider === 'agy') {
|
|
147
|
+
metrics = extractAgyMetrics(agentData.usage, agentName);
|
|
148
|
+
} else if (provider === 'codex') {
|
|
149
|
+
metrics = extractCodexMetrics(agentData.usage, agentName);
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
// Update DOM for each metric
|
|
@@ -155,7 +156,8 @@ ${metricExtractors}
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// Update agent card error state
|
|
158
|
-
const agentCard = document.
|
|
159
|
+
const agentCard = Array.from(document.querySelectorAll('.agent-card'))
|
|
160
|
+
.find(card => card.dataset.agentId === agentName);
|
|
159
161
|
if (agentCard) {
|
|
160
162
|
if (agentData.error) {
|
|
161
163
|
agentCard.classList.add('error');
|
package/src/client-script.js
CHANGED
|
@@ -107,7 +107,7 @@ const clientScript = faviconScript + `
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
try {
|
|
110
|
-
await fetch('/refresh/' + agent, { method: 'POST' });
|
|
110
|
+
await fetch('/refresh/' + encodeURIComponent(agent), { method: 'POST' });
|
|
111
111
|
// Use XHR update instead of page reload
|
|
112
112
|
await performAutoRefresh();
|
|
113
113
|
if (isIconBtn) {
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const { extractSnapshotMetrics } = require('./snapshot-metrics.js');
|
|
|
15
15
|
const { attachPaceEvaluation } = require('./pace-attachment.js');
|
|
16
16
|
const { KeepaliveManager } = require('./keepalive-manager.js');
|
|
17
17
|
const { AutoRefreshManager } = require('./auto-refresh-manager.js');
|
|
18
|
+
const { normalizeAgentSpecs } = require('./agent-config.js');
|
|
18
19
|
|
|
19
20
|
// Load package.json for version info
|
|
20
21
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
@@ -250,13 +251,15 @@ class AgentTank {
|
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
|
|
254
|
+
const agentSpecs = normalizeAgentSpecs(agentNames);
|
|
255
|
+
|
|
253
256
|
// Create agent instances
|
|
254
|
-
for (const
|
|
255
|
-
const agent = this.createAgent(
|
|
257
|
+
for (const spec of agentSpecs) {
|
|
258
|
+
const agent = this.createAgent(spec);
|
|
256
259
|
if (agent) {
|
|
257
260
|
agent.freshProcess = this.freshProcess;
|
|
258
|
-
this.agents.set(
|
|
259
|
-
logger.agent(
|
|
261
|
+
this.agents.set(spec.id, agent);
|
|
262
|
+
logger.agent(spec.provider, `Created agent ${spec.id}${this.freshProcess ? ' (fresh process mode)' : ''}`);
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
265
|
|
|
@@ -324,18 +327,31 @@ class AgentTank {
|
|
|
324
327
|
}
|
|
325
328
|
}
|
|
326
329
|
|
|
327
|
-
createAgent(
|
|
328
|
-
|
|
330
|
+
createAgent(input) {
|
|
331
|
+
const spec = typeof input === 'string'
|
|
332
|
+
? { provider: input, id: input, alias: null, configPath: null }
|
|
333
|
+
: input;
|
|
334
|
+
let agent;
|
|
335
|
+
switch (spec.provider) {
|
|
329
336
|
case 'claude':
|
|
330
|
-
|
|
337
|
+
agent = new ClaudeAgent({ useApi: this.claudeApi, configPath: spec.configPath });
|
|
338
|
+
break;
|
|
331
339
|
case 'agy':
|
|
332
|
-
|
|
340
|
+
agent = new AgyAgent({ configPath: spec.configPath });
|
|
341
|
+
break;
|
|
333
342
|
case 'codex':
|
|
334
|
-
|
|
343
|
+
agent = new CodexAgent({ configPath: spec.configPath });
|
|
344
|
+
break;
|
|
335
345
|
default:
|
|
336
|
-
logger.warn(`Unknown agent: ${
|
|
346
|
+
logger.warn(`Unknown agent: ${spec.provider}`);
|
|
337
347
|
return null;
|
|
338
348
|
}
|
|
349
|
+
|
|
350
|
+
agent.provider = spec.provider;
|
|
351
|
+
agent.id = spec.id;
|
|
352
|
+
agent.alias = spec.alias;
|
|
353
|
+
agent.configPath = spec.configPath;
|
|
354
|
+
return agent;
|
|
339
355
|
}
|
|
340
356
|
|
|
341
357
|
async refreshAll() {
|
|
@@ -344,7 +360,7 @@ class AgentTank {
|
|
|
344
360
|
}
|
|
345
361
|
|
|
346
362
|
logger.info('๐ Refreshing all agents...');
|
|
347
|
-
const
|
|
363
|
+
const providers = [...new Set(Array.from(this.agents.values(), agent => agent.provider))];
|
|
348
364
|
|
|
349
365
|
// Fetch PTY agent data and public status concurrently
|
|
350
366
|
const [, publicStatusResult] = await Promise.all([
|
|
@@ -359,7 +375,7 @@ class AgentTank {
|
|
|
359
375
|
)
|
|
360
376
|
),
|
|
361
377
|
// Public API status polling
|
|
362
|
-
fetchPublicStatus(
|
|
378
|
+
fetchPublicStatus(providers).catch(err => {
|
|
363
379
|
logger.error('Error fetching public status:', err.message);
|
|
364
380
|
return {};
|
|
365
381
|
}),
|
|
@@ -371,8 +387,8 @@ class AgentTank {
|
|
|
371
387
|
// Record usage snapshots and attach pace evaluations
|
|
372
388
|
for (const [name, agent] of this.agents) {
|
|
373
389
|
if (agent.usage) {
|
|
374
|
-
this._recordSnapshot(name, agent.usage);
|
|
375
|
-
attachPaceEvaluation(
|
|
390
|
+
this._recordSnapshot(name, agent.provider, agent.usage);
|
|
391
|
+
attachPaceEvaluation(agent.provider, agent.usage);
|
|
376
392
|
}
|
|
377
393
|
}
|
|
378
394
|
|
|
@@ -380,12 +396,12 @@ class AgentTank {
|
|
|
380
396
|
}
|
|
381
397
|
|
|
382
398
|
/** Record a usage snapshot to the history store. @private */
|
|
383
|
-
_recordSnapshot(
|
|
399
|
+
_recordSnapshot(agentId, provider, usage) {
|
|
384
400
|
try {
|
|
385
|
-
const snapshot = extractSnapshotMetrics(
|
|
386
|
-
if (snapshot) this.historyStore.addSnapshot(
|
|
401
|
+
const snapshot = extractSnapshotMetrics(provider, usage);
|
|
402
|
+
if (snapshot) this.historyStore.addSnapshot(agentId, snapshot);
|
|
387
403
|
} catch (err) {
|
|
388
|
-
console.error(`[${
|
|
404
|
+
console.error(`[${agentId}] Error recording snapshot:`, err.message);
|
|
389
405
|
}
|
|
390
406
|
}
|
|
391
407
|
|
|
@@ -402,8 +418,8 @@ class AgentTank {
|
|
|
402
418
|
|
|
403
419
|
// Record snapshot and attach pace evaluation for single agent refresh
|
|
404
420
|
if (agent.usage) {
|
|
405
|
-
this._recordSnapshot(name, agent.usage);
|
|
406
|
-
attachPaceEvaluation(
|
|
421
|
+
this._recordSnapshot(name, agent.provider, agent.usage);
|
|
422
|
+
attachPaceEvaluation(agent.provider, agent.usage);
|
|
407
423
|
}
|
|
408
424
|
}
|
|
409
425
|
|
|
@@ -457,14 +473,25 @@ class AgentTank {
|
|
|
457
473
|
getStatus() {
|
|
458
474
|
const status = {};
|
|
459
475
|
for (const [name, agent] of this.agents) {
|
|
460
|
-
status[name] = {
|
|
476
|
+
status[name] = {
|
|
477
|
+
...agent.getStatus(),
|
|
478
|
+
id: name,
|
|
479
|
+
alias: agent.alias,
|
|
480
|
+
provider: agent.provider,
|
|
481
|
+
publicStatus: this.publicStatus[agent.provider] || null,
|
|
482
|
+
};
|
|
461
483
|
}
|
|
462
484
|
return status;
|
|
463
485
|
}
|
|
464
486
|
|
|
465
487
|
getAgentStatus(name) {
|
|
466
488
|
const agent = this.agents.get(name);
|
|
467
|
-
return agent ?
|
|
489
|
+
return agent ? {
|
|
490
|
+
...agent.getStatus(),
|
|
491
|
+
id: name,
|
|
492
|
+
alias: agent.alias,
|
|
493
|
+
provider: agent.provider,
|
|
494
|
+
} : null;
|
|
468
495
|
}
|
|
469
496
|
|
|
470
497
|
authenticate(req, url) {
|
package/src/pace-attachment.js
CHANGED
|
@@ -36,7 +36,7 @@ function attachClaudePace(usage) {
|
|
|
36
36
|
const sections = [
|
|
37
37
|
{ data: usage.session, cycle: 'session' },
|
|
38
38
|
{ data: usage.weeklyAll, cycle: 'weekly' },
|
|
39
|
-
{ data: usage.
|
|
39
|
+
{ data: usage.weeklyFable, cycle: 'weekly' },
|
|
40
40
|
{ data: usage.weekly, cycle: 'weekly' },
|
|
41
41
|
{ data: usage.extraUsage, cycle: 'weekly' }
|
|
42
42
|
];
|
|
@@ -54,7 +54,7 @@ function attachAgyPace(usage) {
|
|
|
54
54
|
if (!usage.models || !Array.isArray(usage.models)) return;
|
|
55
55
|
|
|
56
56
|
for (const model of usage.models) {
|
|
57
|
-
attachPaceToData(model, 'percentUsed', 'sessionAgy');
|
|
57
|
+
attachPaceToData(model, 'percentUsed', model.cycle || 'sessionAgy');
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
package/src/snapshot-metrics.js
CHANGED
|
@@ -13,7 +13,7 @@ function extractClaudeMetrics(usage) {
|
|
|
13
13
|
return {
|
|
14
14
|
session: usage.session?.percent ?? null,
|
|
15
15
|
weeklyAll: usage.weeklyAll?.percent ?? null,
|
|
16
|
-
|
|
16
|
+
weeklyFable: usage.weeklyFable?.percent ?? null,
|
|
17
17
|
weekly: usage.weekly?.percent ?? null,
|
|
18
18
|
extraUsage: usage.extraUsage?.percent ?? null
|
|
19
19
|
};
|
package/src/status-page.js
CHANGED
|
@@ -71,20 +71,25 @@ function renderCardFooter(data) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function renderAgentCard([name, data]) {
|
|
74
|
-
const
|
|
74
|
+
const provider = data.provider || data.name || name;
|
|
75
|
+
const usageHtml = formatUsage(provider, data.usage, { agentId: name });
|
|
75
76
|
const statusClass = data.error ? 'error' : data.isRefreshing ? 'refreshing' : 'ok';
|
|
76
|
-
const icon = agentIcons[
|
|
77
|
-
const
|
|
77
|
+
const icon = agentIcons[provider] || '';
|
|
78
|
+
const providerName = getAgentDisplayName(provider);
|
|
79
|
+
const displayName = data.alias && data.alias !== provider
|
|
80
|
+
? `${providerName} ยท ${data.alias}`
|
|
81
|
+
: providerName;
|
|
78
82
|
const statusBadgeHtml = renderPublicStatusBadge(data.publicStatus);
|
|
79
83
|
const footerHtml = renderCardFooter(data);
|
|
84
|
+
const refreshArgument = escapeHtml(JSON.stringify(name));
|
|
80
85
|
|
|
81
86
|
return `
|
|
82
|
-
<div class="agent-card ${statusClass} agent-${name}">
|
|
87
|
+
<div class="agent-card ${statusClass} agent-${escapeHtml(provider)}" data-agent-id="${escapeHtml(name)}">
|
|
83
88
|
<h2 class="agent-heading">
|
|
84
89
|
${icon}
|
|
85
|
-
<span>${displayName}</span>
|
|
90
|
+
<span>${escapeHtml(displayName)}</span>
|
|
86
91
|
${statusBadgeHtml}
|
|
87
|
-
<button class="refresh-icon-btn" onclick="refresh(
|
|
92
|
+
<button class="refresh-icon-btn" onclick="refresh(${refreshArgument}, event)" ${data.isRefreshing ? 'disabled' : ''} title="Refresh ${escapeHtml(displayName)}">
|
|
88
93
|
${refreshIcon}
|
|
89
94
|
</button>
|
|
90
95
|
</h2>
|