Merge branch 'feat/cli-usage-reporting-50'

This commit is contained in:
Brad Groux 2026-02-05 17:56:09 -06:00
commit d52ec5b7de
3 changed files with 335 additions and 0 deletions

277
cli/src/commands/usage.ts Normal file
View file

@ -0,0 +1,277 @@
import { Command } from 'commander';
import chalk from 'chalk';
import { api } from '../utils/api.js';
import { findTask } from '../utils/find.js';
import type {
TokenMetrics,
DurationMetrics,
TaskCostMetrics,
TaskCostEntry,
} from '../utils/types.js';
/**
* Format a number as currency (e.g., "$0.42")
*/
function formatCost(cost: number): string {
return `$${cost.toFixed(2)}`;
}
/**
* Format a duration in milliseconds to human-readable format
*/
function formatDuration(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
}
if (minutes > 0) {
const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds}s`;
}
return `${seconds}s`;
}
/**
* Format large numbers with commas (e.g., 1,234,567)
*/
function formatNumber(num: number): string {
return num.toLocaleString('en-US');
}
/**
* Display usage summary (default behavior)
*/
async function displayUsageSummary(period: string, json: boolean): Promise<void> {
try {
// Fetch token metrics
const tokenMetrics = await api<TokenMetrics>(`/api/metrics/tokens?period=${period}`);
// Fetch duration metrics
const durationMetrics = await api<DurationMetrics>(`/api/metrics/duration?period=${period}`);
if (json) {
console.log(
JSON.stringify(
{
period,
tokens: tokenMetrics,
duration: durationMetrics,
},
null,
2
)
);
return;
}
// Display summary in table format
console.log(chalk.bold(`\n📊 Usage Summary (${period})\n`));
console.log(chalk.dim('─'.repeat(60)));
// Token usage
console.log(chalk.bold('\n💬 Token Usage'));
console.log(` Total Tokens: ${chalk.cyan(formatNumber(tokenMetrics.totalTokens))}`);
console.log(` Input Tokens: ${chalk.white(formatNumber(tokenMetrics.inputTokens))}`);
console.log(` Output Tokens: ${chalk.white(formatNumber(tokenMetrics.outputTokens))}`);
if (tokenMetrics.cacheTokens > 0) {
console.log(` Cache Tokens: ${chalk.green(formatNumber(tokenMetrics.cacheTokens))}`);
}
console.log(` Runs: ${chalk.white(formatNumber(tokenMetrics.runs))}`);
// Cost estimation (simple: $0.01/1K input, $0.03/1K output)
const estimatedCost =
(tokenMetrics.inputTokens / 1000) * 0.01 + (tokenMetrics.outputTokens / 1000) * 0.03;
console.log(` Estimated Cost: ${chalk.yellow(formatCost(estimatedCost))}`);
// Duration metrics
console.log(chalk.bold('\n⏱️ Time Spent'));
console.log(` Average: ${chalk.cyan(formatDuration(durationMetrics.avgMs))}`);
console.log(` Median: ${chalk.white(formatDuration(durationMetrics.p50Ms))}`);
console.log(` 95th %ile: ${chalk.white(formatDuration(durationMetrics.p95Ms))}`);
console.log(chalk.dim('\n─'.repeat(60)));
console.log(chalk.dim(`\n💡 Tip: Use --agent <name> or --task <id> for detailed breakdowns\n`));
} catch (err) {
console.error(chalk.red(`Error: ${(err as Error).message}`));
process.exit(1);
}
}
/**
* Display per-agent usage breakdown
*/
async function displayAgentUsage(agentName: string, period: string, json: boolean): Promise<void> {
try {
const tokenMetrics = await api<TokenMetrics>(`/api/metrics/tokens?period=${period}`);
const durationMetrics = await api<DurationMetrics>(`/api/metrics/duration?period=${period}`);
// Find agent in breakdown
const agentTokens = tokenMetrics.byAgent.find((a) => a.agent === agentName);
const agentDuration = durationMetrics.byAgent.find((a) => a.agent === agentName);
if (!agentTokens && !agentDuration) {
console.error(chalk.red(`No data found for agent: ${agentName}`));
console.log(chalk.dim('\nAvailable agents:'));
tokenMetrics.byAgent.forEach((a) => console.log(chalk.dim(` - ${a.agent}`)));
process.exit(1);
}
if (json) {
console.log(
JSON.stringify(
{
agent: agentName,
period,
tokens: agentTokens || null,
duration: agentDuration || null,
},
null,
2
)
);
return;
}
console.log(chalk.bold(`\n📊 Agent Usage: ${agentName} (${period})\n`));
console.log(chalk.dim('─'.repeat(60)));
if (agentTokens) {
console.log(chalk.bold('\n💬 Token Usage'));
console.log(` Total Tokens: ${chalk.cyan(formatNumber(agentTokens.totalTokens))}`);
console.log(` Input Tokens: ${chalk.white(formatNumber(agentTokens.inputTokens))}`);
console.log(` Output Tokens: ${chalk.white(formatNumber(agentTokens.outputTokens))}`);
if (agentTokens.cacheTokens > 0) {
console.log(` Cache Tokens: ${chalk.green(formatNumber(agentTokens.cacheTokens))}`);
}
console.log(` Runs: ${chalk.white(formatNumber(agentTokens.runs))}`);
const estimatedCost =
(agentTokens.inputTokens / 1000) * 0.01 + (agentTokens.outputTokens / 1000) * 0.03;
console.log(` Estimated Cost: ${chalk.yellow(formatCost(estimatedCost))}`);
}
if (agentDuration) {
console.log(chalk.bold('\n⏱️ Time Spent'));
console.log(` Runs: ${chalk.white(formatNumber(agentDuration.runs))}`);
console.log(` Average: ${chalk.cyan(formatDuration(agentDuration.avgMs))}`);
console.log(` Median: ${chalk.white(formatDuration(agentDuration.p50Ms))}`);
console.log(` 95th %ile: ${chalk.white(formatDuration(agentDuration.p95Ms))}`);
}
console.log(chalk.dim('\n─'.repeat(60) + '\n'));
} catch (err) {
console.error(chalk.red(`Error: ${(err as Error).message}`));
process.exit(1);
}
}
/**
* Display per-task usage breakdown
*/
async function displayTaskUsage(taskId: string, period: string, json: boolean): Promise<void> {
try {
// Find task to get full ID
const task = await findTask(taskId);
if (!task) {
console.error(chalk.red(`Task not found: ${taskId}`));
process.exit(1);
}
// Fetch task cost data
const taskCostMetrics = await api<TaskCostMetrics>(`/api/metrics/task-cost?period=${period}`);
// Find this specific task
const taskCost = taskCostMetrics.tasks.find((t) => t.taskId === task.id);
if (!taskCost) {
console.error(chalk.red(`No usage data found for task: ${task.title}`));
console.log(chalk.dim('\nThis task may not have any activity in the selected period.'));
process.exit(1);
}
if (json) {
console.log(
JSON.stringify(
{
taskId: task.id,
taskTitle: task.title,
period,
usage: taskCost,
},
null,
2
)
);
return;
}
console.log(chalk.bold(`\n📊 Task Usage: ${task.title}\n`));
console.log(chalk.dim(` ID: ${task.id}`));
console.log(chalk.dim('─'.repeat(60)));
console.log(chalk.bold('\n💬 Token Usage'));
console.log(` Total Tokens: ${chalk.cyan(formatNumber(taskCost.totalTokens))}`);
console.log(` Input Tokens: ${chalk.white(formatNumber(taskCost.inputTokens))}`);
console.log(` Output Tokens: ${chalk.white(formatNumber(taskCost.outputTokens))}`);
console.log(chalk.bold('\n💰 Cost'));
console.log(` Total Cost: ${chalk.yellow(formatCost(taskCost.estimatedCost))}`);
console.log(` Runs: ${chalk.white(formatNumber(taskCost.runs))}`);
console.log(` Avg Cost/Run: ${chalk.white(formatCost(taskCost.avgCostPerRun))}`);
console.log(chalk.dim('\n─'.repeat(60) + '\n'));
} catch (err) {
console.error(chalk.red(`Error: ${(err as Error).message}`));
process.exit(1);
}
}
export function registerUsageCommands(program: Command): void {
const usage = program
.command('usage')
.description('Display usage statistics (tokens, costs, time)')
.option(
'--period <period>',
'Time period: today, 24h, 3d, 7d, 30d, 3m, 6m, 12m, wtd, mtd, ytd',
'7d'
)
.option('--agent <name>', 'Show usage breakdown for a specific agent')
.option('--task <id>', 'Show usage breakdown for a specific task')
.option('--json', 'Output as JSON')
.action(async (options) => {
const period = options.period;
// Validate period
const validPeriods = [
'today',
'24h',
'3d',
'7d',
'30d',
'3m',
'6m',
'12m',
'wtd',
'mtd',
'ytd',
];
if (!validPeriods.includes(period)) {
console.error(
chalk.red(`Invalid period: ${period}. Must be one of: ${validPeriods.join(', ')}`)
);
process.exit(1);
}
if (options.task) {
await displayTaskUsage(options.task, period, options.json);
} else if (options.agent) {
await displayAgentUsage(options.agent, period, options.json);
} else {
await displayUsageSummary(period, options.json);
}
});
}

View file

@ -13,6 +13,7 @@ import { registerAgentStatusCommands } from './commands/agent-status.js';
import { registerProjectCommands } from './commands/projects.js';
import { registerWorkflowCommands } from './commands/workflow.js';
import { registerSetupCommands } from './commands/setup.js';
import { registerUsageCommands } from './commands/usage.js';
const program = new Command();
@ -35,5 +36,6 @@ registerAgentStatusCommands(program);
registerProjectCommands(program);
registerWorkflowCommands(program);
registerSetupCommands(program);
registerUsageCommands(program);
program.parse();

View file

@ -1,2 +1,58 @@
// Re-export shared types
export type { Task } from '@veritas-kanban/shared';
// Metrics types
export interface TokenMetrics {
period: string;
totalTokens: number;
inputTokens: number;
outputTokens: number;
cacheTokens: number;
runs: number;
perSuccessfulRun: {
avg: number;
p50: number;
p95: number;
};
byAgent: Array<{
agent: string;
totalTokens: number;
inputTokens: number;
outputTokens: number;
cacheTokens: number;
runs: number;
}>;
}
export interface DurationMetrics {
period: string;
runs: number;
avgMs: number;
p50Ms: number;
p95Ms: number;
byAgent: Array<{
agent: string;
runs: number;
avgMs: number;
p50Ms: number;
p95Ms: number;
}>;
}
export interface TaskCostEntry {
taskId: string;
taskTitle?: string;
inputTokens: number;
outputTokens: number;
totalTokens: number;
estimatedCost: number;
runs: number;
avgCostPerRun: number;
}
export interface TaskCostMetrics {
period: string;
tasks: TaskCostEntry[];
totalCost: number;
avgCostPerTask: number;
}