Files
oh-my-pi/packages/coding-agent/src/cli/stats-cli.ts
T
maximhar 7ea7b53783 feat(copilot): track premium requests with model multipliers (#255)
* Track Copilot premium requests with model multipliers

* refactor(copilot): source premium multipliers from models.json

* feat(copilot): add gpt-5.3-codex bundled model

* fix(stats): round premium request totals in summary

* fix(stats): round premium requests in sync summary
2026-03-03 03:26:00 +01:00

183 lines
5.7 KiB
TypeScript

/**
* Stats CLI command handlers.
*
* Handles `omp stats` subcommand for viewing AI usage statistics.
*/
import { APP_NAME, formatDuration, formatNumber, formatPercent } from "@oh-my-pi/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<void> {
// Lazy import to avoid loading stats module when not needed
const { getDashboardStats, syncAllSessions, getTotalMessageCount, startServer, closeDb } = await import(
"@oh-my-pi/omp-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<void> {
const { getDashboardStats } = await import("@oh-my-pi/omp-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> 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
`);
}