/** * Stats CLI command handlers. * * Handles `xcsh stats` subcommand for viewing AI usage statistics. */ import { APP_NAME, formatDuration, formatNumber, formatPercent } from "@f5-sales-demo/pi-utils"; import chalk from "chalk"; import { openPath } from "../utils/open"; // ============================================================================= // Types // ============================================================================= export interface StatsCommandArgs { port: number; json: boolean; summary: boolean; } // ============================================================================= // Argument Parser // ============================================================================= /** * Parse stats subcommand arguments. * Returns undefined if not a stats command. */ export function parseStatsArgs(args: string[]): StatsCommandArgs | undefined { if (args.length === 0 || args[0] !== "stats") { return undefined; } const result: StatsCommandArgs = { port: 3847, json: false, summary: false, }; for (let i = 1; i < args.length; i++) { const arg = args[i]; if (arg === "--json" || arg === "-j") { result.json = true; } else if (arg === "--summary" || arg === "-s") { result.summary = true; } else if ((arg === "--port" || arg === "-p") && i + 1 < args.length) { result.port = parseInt(args[++i], 10); } else if (arg.startsWith("--port=")) { result.port = parseInt(arg.split("=")[1], 10); } } return result; } function formatCost(n: number): string { if (n < 0.01) return `$${n.toFixed(4)}`; if (n < 1) return `$${n.toFixed(3)}`; return `$${n.toFixed(2)}`; } function normalizePremiumRequests(n: number): number { return Math.round((n + Number.EPSILON) * 100) / 100; } // ============================================================================= // Command Handler // ============================================================================= export async function runStatsCommand(cmd: StatsCommandArgs): Promise { // Lazy import to avoid loading stats module when not needed const { getDashboardStats, syncAllSessions, getTotalMessageCount, startServer, closeDb } = await import( "@f5-sales-demo/xcsh-stats" ); // Sync session files first console.log("Syncing session files..."); const { processed, files } = await syncAllSessions(); const total = await getTotalMessageCount(); console.log(`Synced ${processed} new entries from ${files} files (${total} total)\n`); if (cmd.json) { const stats = await getDashboardStats(); console.log(JSON.stringify(stats, null, 2)); return; } if (cmd.summary) { await printStatsSummary(); return; } // Start the dashboard server const { port } = await startServer(cmd.port); console.log(chalk.green(`Dashboard available at: http://localhost:${port}`)); // Open browser const url = `http://localhost:${port}`; openPath(url); console.log("Press Ctrl+C to stop\n"); // Keep process running process.on("SIGINT", () => { console.log("\nShutting down..."); closeDb(); process.exit(0); }); // Keep the process alive await new Promise(() => {}); } async function printStatsSummary(): Promise { const { getDashboardStats } = await import("@f5-sales-demo/xcsh-stats"); const stats = await getDashboardStats(); const { overall, byModel, byFolder } = stats; console.log(chalk.bold("\n=== AI Usage Statistics ===\n")); console.log(chalk.bold("Overall:")); console.log(` Requests: ${formatNumber(overall.totalRequests)} (${formatNumber(overall.failedRequests)} errors)`); console.log(` Error Rate: ${formatPercent(overall.errorRate)}`); console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`); console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`); console.log(` Total Cost: ${formatCost(overall.totalCost)}`); console.log(` Premium Requests: ${formatNumber(normalizePremiumRequests(overall.totalPremiumRequests ?? 0))}`); console.log(` Avg Duration: ${overall.avgDuration !== null ? formatDuration(overall.avgDuration) : "-"}`); console.log(` Avg TTFT: ${overall.avgTtft !== null ? formatDuration(overall.avgTtft) : "-"}`); if (overall.avgTokensPerSecond !== null) { console.log(` Avg Tokens/s: ${overall.avgTokensPerSecond.toFixed(1)}`); } if (byModel.length > 0) { console.log(chalk.bold("\nBy Model:")); for (const m of byModel.slice(0, 10)) { console.log( ` ${m.model}: ${formatNumber(m.totalRequests)} reqs, ${formatCost(m.totalCost)}, ${formatPercent(m.cacheRate)} cache`, ); } } if (byFolder.length > 0) { console.log(chalk.bold("\nBy Folder:")); for (const f of byFolder.slice(0, 10)) { console.log(` ${f.folder}: ${formatNumber(f.totalRequests)} reqs, ${formatCost(f.totalCost)}`); } } console.log(""); } // ============================================================================= // Help // ============================================================================= export function printStatsHelp(): void { console.log(`${chalk.bold(`${APP_NAME} stats`)} - AI Usage Statistics Dashboard ${chalk.bold("Usage:")} ${APP_NAME} stats [options] ${chalk.bold("Options:")} -p, --port Port for the dashboard server (default: 3847) -j, --json Output stats as JSON and exit -s, --summary Print summary to console and exit -h, --help Show this help message ${chalk.bold("Examples:")} ${APP_NAME} stats # Start dashboard server ${APP_NAME} stats --json # Print stats as JSON ${APP_NAME} stats --summary # Print summary to console ${APP_NAME} stats --port 8080 # Start on custom port ${chalk.bold("Metrics:")} - Total requests and error rate - Token usage (input, output, cache) - Cost breakdown - Average duration and time to first token (TTFT) - Tokens per second throughput `); }