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.
@@ -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', ['-s', 'read-only', '-a', 'untrusted', 'app-server'], {
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) { logger.agent(this.name, 'Sending /status command...'); setTimeout(() => { if (shell) shell.write('/status\r'); }, 100); }
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: { ...process.env, TERM: 'xterm-256color' },
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.write('/status');
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
- if (output.includes('5h limit') && output.includes('Weekly limit')) return true;
268
- return false;
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: { ...process.env, TERM: 'xterm-256color' },
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 = path.join(homeDir, '.claude', '.credentials.json');
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 = path.join(homeDir, '.claude', '.credentials.json');
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, weeklySonnet: null };
256
+ const usage = { session: null, weeklyAll: null, weeklyFable: null };
252
257
 
253
- // Parse session
254
- const sessionData = parseUsageSection(extractSection(clean, 'Current\\s*session', 'Current\\s*week'));
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*\\)?', 'Current\\s*week\\s*\\(?\\s*Son'));
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 (Sonnet only)
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*\\('));
269
- if (weeklySonnetData) {
270
- usage.weeklySonnet = { label: 'Current week (Sonnet only)', ...weeklySonnetData };
271
- addPaceData(usage.weeklySonnet, 'weekly');
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.weeklySonnet) {
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.weeklySonnet, label: 'Weekly (Sonnet)', cycle: 'weekly' },
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 = \`claude-\${label.toLowerCase().replace(/[^a-z0-9]/g, '-')}\`;
62
+ const metricId = \`\${agentId}-\${label.toLowerCase().replace(/[^a-z0-9]/g, '-')}\`;
63
63
  metrics.push({
64
64
  metricId,
65
- agent: 'claude',
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 = \`agy-\${modelName.toLowerCase().replace(/[^a-z0-9]/g, '-')}\`;
83
+ const metricId = \`\${agentId}-\${modelName.toLowerCase().replace(/[^a-z0-9]/g, '-')}\`;
84
84
  metrics.push({
85
85
  metricId,
86
- agent: 'agy',
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: \`codex-\${modelSlug}-5h\`,
113
- agent: 'codex',
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: \`codex-\${modelSlug}-weekly\`,
125
- agent: 'codex',
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 (agentName === 'claude') {
144
- metrics = extractClaudeMetrics(agentData.usage);
145
- } else if (agentName === 'agy') {
146
- metrics = extractAgyMetrics(agentData.usage);
147
- } else if (agentName === 'codex') {
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.querySelector(\`.agent-card.agent-\${agentName}\`);
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');
@@ -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 name of agentNames) {
255
- const agent = this.createAgent(name);
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(name, agent);
259
- logger.agent(name, `Created agent${this.freshProcess ? ' (fresh process mode)' : ''}`);
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(name) {
328
- switch (name) {
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
- return new ClaudeAgent({ useApi: this.claudeApi });
337
+ agent = new ClaudeAgent({ useApi: this.claudeApi, configPath: spec.configPath });
338
+ break;
331
339
  case 'agy':
332
- return new AgyAgent();
340
+ agent = new AgyAgent({ configPath: spec.configPath });
341
+ break;
333
342
  case 'codex':
334
- return new CodexAgent();
343
+ agent = new CodexAgent({ configPath: spec.configPath });
344
+ break;
335
345
  default:
336
- logger.warn(`Unknown agent: ${name}`);
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 agentNames = Array.from(this.agents.keys());
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(agentNames).catch(err => {
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(name, agent.usage);
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(agentName, usage) {
399
+ _recordSnapshot(agentId, provider, usage) {
384
400
  try {
385
- const snapshot = extractSnapshotMetrics(agentName, usage);
386
- if (snapshot) this.historyStore.addSnapshot(agentName, snapshot);
401
+ const snapshot = extractSnapshotMetrics(provider, usage);
402
+ if (snapshot) this.historyStore.addSnapshot(agentId, snapshot);
387
403
  } catch (err) {
388
- console.error(`[${agentName}] Error recording snapshot:`, err.message);
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(name, agent.usage);
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] = { ...agent.getStatus(), publicStatus: this.publicStatus[name] || null };
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 ? agent.getStatus() : null;
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) {
@@ -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.weeklySonnet, cycle: 'weekly' },
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
 
@@ -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
- weeklySonnet: usage.weeklySonnet?.percent ?? null,
16
+ weeklyFable: usage.weeklyFable?.percent ?? null,
17
17
  weekly: usage.weekly?.percent ?? null,
18
18
  extraUsage: usage.extraUsage?.percent ?? null
19
19
  };
@@ -71,20 +71,25 @@ function renderCardFooter(data) {
71
71
  }
72
72
 
73
73
  function renderAgentCard([name, data]) {
74
- const usageHtml = formatUsage(name, data.usage);
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[name] || '';
77
- const displayName = getAgentDisplayName(name);
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('${name}', event)" ${data.isRefreshing ? 'disabled' : ''} title="Refresh ${displayName}">
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>