feat: add token cost estimation and improve token formatting

- Format tokens: 1000k → 1m, 1450k → 1.45m, etc.
- Estimate cost using Claude Sonnet pricing ($3/M input, $15/M output)
- Show estimated cost in respawn banner and header
- More accurate than stored cost which doesn't update in interactive mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-25 20:12:16 +01:00
co-authored by Claude Opus 4.5
parent 4eb361f204
commit 9d32fd594b
3 changed files with 48 additions and 16 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ When user says "COM": 1) Increment version in BOTH `package.json` AND `CLAUDE.md
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
**Version**: 0.1361 (must match `package.json`)
**Version**: 0.1362 (must match `package.json`)
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1361",
"version": "0.1362",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+46 -14
View File
@@ -417,6 +417,26 @@ class ClaudemanApp {
return this._elemCache[id];
}
// Format token count: 1000k -> 1m, 1450k -> 1.45m, 500 -> 500
formatTokens(count) {
if (count >= 1000000) {
const m = count / 1000000;
return m >= 10 ? `${m.toFixed(1)}m` : `${m.toFixed(2)}m`;
} else if (count >= 1000) {
const k = count / 1000;
return k >= 100 ? `${k.toFixed(0)}k` : `${k.toFixed(1)}k`;
}
return String(count);
}
// Estimate cost from tokens using Claude Sonnet pricing
// Input: $3/M tokens, Output: $15/M tokens
estimateCost(inputTokens, outputTokens) {
const inputCost = (inputTokens / 1000000) * 3;
const outputCost = (outputTokens / 1000000) * 15;
return inputCost + outputCost;
}
init() {
this.initTerminal();
this.loadFontSize();
@@ -738,7 +758,7 @@ class ClaudemanApp {
this.updateCost();
// Update tokens display if this is the active session
if (session.id === this.activeSessionId && session.tokens) {
this.updateRespawnTokens(session.tokens.total);
this.updateRespawnTokens(session.tokens);
}
});
@@ -2086,7 +2106,7 @@ class ClaudemanApp {
// Show tokens if session has token data
const session = this.sessions.get(this.activeSessionId);
if (session && session.tokens) {
this.updateRespawnTokens(session.tokens.total);
this.updateRespawnTokens(session.tokens);
}
}
@@ -2234,11 +2254,19 @@ class ClaudemanApp {
this.$('respawnTimer').textContent = this.formatTime(remaining);
}
updateRespawnTokens(totalTokens) {
updateRespawnTokens(tokens) {
const tokensEl = this.$('respawnTokens');
if (totalTokens > 0) {
// Support both old format (number) and new format (object with input/output/total)
const isObject = tokens && typeof tokens === 'object';
const total = isObject ? tokens.total : tokens;
const input = isObject ? (tokens.input || 0) : Math.round(total * 0.6);
const output = isObject ? (tokens.output || 0) : Math.round(total * 0.4);
if (total > 0) {
tokensEl.style.display = '';
tokensEl.textContent = `${(totalTokens / 1000).toFixed(1)}k tokens`;
const tokenStr = this.formatTokens(total);
const estimatedCost = this.estimateCost(input, output);
tokensEl.textContent = `${tokenStr} tokens · $${estimatedCost.toFixed(2)}`;
} else {
tokensEl.style.display = 'none';
}
@@ -2547,28 +2575,32 @@ class ClaudemanApp {
updateTokens() {
// Use global stats if available (includes deleted sessions)
let total = 0;
let totalInput = 0;
let totalOutput = 0;
if (this.globalStats) {
total = this.globalStats.totalInputTokens + this.globalStats.totalOutputTokens;
totalInput = this.globalStats.totalInputTokens || 0;
totalOutput = this.globalStats.totalOutputTokens || 0;
} else {
// Fallback to active sessions only
this.sessions.forEach(s => {
if (s.tokens) {
total += s.tokens.total || 0;
totalInput += s.tokens.input || 0;
totalOutput += s.tokens.output || 0;
}
});
}
const total = totalInput + totalOutput;
this.totalTokens = total;
const display = total >= 1000 ? `${(total / 1000).toFixed(1)}k` : total;
const display = this.formatTokens(total);
// Show global cost if available
const costDisplay = this.globalStats ? `$${this.globalStats.totalCost.toFixed(2)}` : '';
// Estimate cost from tokens (more accurate than stored cost in interactive mode)
const estimatedCost = this.estimateCost(totalInput, totalOutput);
const tokenEl = this.$('headerTokens');
if (tokenEl) {
tokenEl.textContent = costDisplay ? `${display} tokens · ${costDisplay}` : `${display} tokens`;
tokenEl.textContent = total > 0 ? `${display} tokens · $${estimatedCost.toFixed(2)}` : '0 tokens';
tokenEl.title = this.globalStats
? `Lifetime: ${this.globalStats.totalSessionsCreated} sessions created`
: 'Token usage across active sessions';
? `Lifetime: ${this.globalStats.totalSessionsCreated} sessions created\nEstimated cost based on Claude Sonnet pricing`
: 'Token usage across active sessions\nEstimated cost based on Claude Sonnet pricing';
}
}