UNPKG

ccusage-live

Version:

Enhanced Claude Code usage analysis tool with live team monitoring and collaboration features

1,209 lines 171 kB
#!/usr/bin/env node import { BLOCKS_COMPACT_WIDTH_THRESHOLD, BLOCKS_DEFAULT_TERMINAL_WIDTH, BLOCKS_WARNING_THRESHOLD, BURN_RATE_THRESHOLDS, DEFAULT_RECENT_DAYS, DEFAULT_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, MCP_DEFAULT_PORT, MIN_REFRESH_INTERVAL_SECONDS, MIN_RENDER_INTERVAL_MS, PROJECT_ALIASES_ENV, PricingFetcher, __toESM, require_usingCtx } from "./pricing-fetcher-C4VsnO1-.js"; import { getTotalTokens } from "./_token-utils-WjkbrjKv.js"; import { CostModes, SortOrders, filterDateSchema } from "./_types-Dbrgtaqy.js"; import { calculateTotals, createTotalsObject } from "./calculate-cost-BDqO4yWA.js"; import { DEFAULT_SESSION_DURATION_HOURS, calculateBurnRate, calculateCostForEntry, createUniqueHash, filterRecentBlocks, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, identifySessionBlocks, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, projectBlockUsage, sortFilesByTimestamp, usageDataSchema } from "./data-loader-B7Yx3ZuE.js"; import { description, log, logger, name, version } from "./logger-D3prNztu.js"; import { detectMismatches, printMismatchReport } from "./debug-B9wnNfnd.js"; import { performUpdateCheck } from "./update-checker-D9Zims3o.js"; import { createMcpHttpApp, createMcpServer, startMcpServerStdio } from "./mcp-DulYSaLX.js"; import { z } from "zod"; import { readFile } from "node:fs/promises"; import process$1 from "node:process"; import { Result } from "@praha/byethrow"; import { uniq } from "es-toolkit"; import F from "node:os"; import { cli, define } from "gunshi"; import pc from "picocolors"; import Table from "cli-table3"; import stringWidth from "string-width"; import * as ansiEscapes$2 from "ansi-escapes"; import * as ansiEscapes$1 from "ansi-escapes"; import * as ansiEscapes from "ansi-escapes"; import prettyMs from "pretty-ms"; import prompts from "prompts"; import { createClient } from "@supabase/supabase-js"; import crypto from "node:crypto"; import { nanoid } from "nanoid"; import { serve } from "@hono/node-server"; /** * Parses and validates a date argument in YYYYMMDD format * @param value - Date string to parse * @returns Validated date string * @throws TypeError if date format is invalid */ function parseDateArg(value) { const result = filterDateSchema.safeParse(value); if (!result.success) throw new TypeError(result.error.issues[0]?.message ?? "Invalid date format"); return result.data; } /** * Shared command line arguments used across multiple CLI commands */ const sharedArgs = { since: { type: "custom", short: "s", description: "Filter from date (YYYYMMDD format)", parse: parseDateArg }, until: { type: "custom", short: "u", description: "Filter until date (YYYYMMDD format)", parse: parseDateArg }, json: { type: "boolean", short: "j", description: "Output in JSON format", default: false }, mode: { type: "enum", short: "m", description: "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)", default: "auto", choices: CostModes }, debug: { type: "boolean", short: "d", description: "Show pricing mismatch information for debugging", default: false }, debugSamples: { type: "number", description: "Number of sample discrepancies to show in debug output (default: 5)", default: 5 }, order: { type: "enum", short: "o", description: "Sort order: desc (newest first) or asc (oldest first)", default: "asc", choices: SortOrders }, breakdown: { type: "boolean", short: "b", description: "Show per-model cost breakdown", default: false }, offline: { type: "boolean", negatable: true, short: "O", description: "Use cached pricing data for Claude models instead of fetching from API", default: false }, color: { type: "boolean", description: "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect." }, noColor: { type: "boolean", description: "Disable colored output (default: auto). NO_COLOR=1 has the same effect." } }; /** * Shared command configuration for Gunshi CLI commands */ const sharedCommandConfig = { args: sharedArgs, toKebab: true }; /** * Responsive table class that adapts column widths based on terminal size * Automatically adjusts formatting and layout for different screen sizes */ var ResponsiveTable = class { head; rows = []; colAligns; style; dateFormatter; compactHead; compactColAligns; compactThreshold; compactMode = false; /** * Creates a new responsive table instance * @param options - Table configuration options */ constructor(options) { this.head = options.head; this.colAligns = options.colAligns ?? Array.from({ length: this.head.length }, () => "left"); this.style = options.style; this.dateFormatter = options.dateFormatter; this.compactHead = options.compactHead; this.compactColAligns = options.compactColAligns; this.compactThreshold = options.compactThreshold ?? 100; } /** * Adds a row to the table * @param row - Row data to add */ push(row) { this.rows.push(row); } /** * Filters a row to compact mode columns * @param row - Row to filter * @param compactIndices - Indices of columns to keep in compact mode * @returns Filtered row */ filterRowToCompact(row, compactIndices) { return compactIndices.map((index) => row[index] ?? ""); } /** * Gets the current table head and col aligns based on compact mode * @returns Current head and colAligns arrays */ getCurrentTableConfig() { if (this.compactMode && this.compactHead != null && this.compactColAligns != null) return { head: this.compactHead, colAligns: this.compactColAligns }; return { head: this.head, colAligns: this.colAligns }; } /** * Gets indices mapping from full table to compact table * @returns Array of column indices to keep in compact mode */ getCompactIndices() { if (this.compactHead == null || !this.compactMode) return Array.from({ length: this.head.length }, (_, i) => i); return this.compactHead.map((compactHeader) => { const index = this.head.indexOf(compactHeader); if (index < 0) { console.warn(`Warning: Compact header "${compactHeader}" not found in table headers [${this.head.join(", ")}]. Using first column as fallback.`); return 0; } return index; }); } /** * Returns whether the table is currently in compact mode * @returns True if compact mode is active */ isCompactMode() { return this.compactMode; } /** * Renders the table as a formatted string * Automatically adjusts layout based on terminal width * @returns Formatted table string */ toString() { const terminalWidth = Number.parseInt(process$1.env.COLUMNS ?? "", 10) || process$1.stdout.columns || 120; this.compactMode = terminalWidth < this.compactThreshold && this.compactHead != null; const { head, colAligns } = this.getCurrentTableConfig(); const compactIndices = this.getCompactIndices(); const dataRows = this.rows.filter((row) => !this.isSeparatorRow(row)); const processedDataRows = this.compactMode ? dataRows.map((row) => this.filterRowToCompact(row, compactIndices)) : dataRows; const allRows = [head.map(String), ...processedDataRows.map((row) => row.map((cell) => { if (typeof cell === "object" && cell != null && "content" in cell) return String(cell.content); return String(cell ?? ""); }))]; const contentWidths = head.map((_, colIndex) => { const maxLength = Math.max(...allRows.map((row) => stringWidth(String(row[colIndex] ?? "")))); return maxLength; }); const numColumns = head.length; const tableOverhead = 3 * numColumns + 1; const availableWidth = terminalWidth - tableOverhead; const columnWidths = contentWidths.map((width, index) => { const align = colAligns[index]; if (align === "right") return Math.max(width + 3, 11); else if (index === 1) return Math.max(width + 2, 15); return Math.max(width + 2, 10); }); const totalRequiredWidth = columnWidths.reduce((sum, width) => sum + width, 0) + tableOverhead; if (totalRequiredWidth > terminalWidth) { const scaleFactor = availableWidth / columnWidths.reduce((sum, width) => sum + width, 0); const adjustedWidths = columnWidths.map((width, index) => { const align = colAligns[index]; let adjustedWidth = Math.floor(width * scaleFactor); if (align === "right") adjustedWidth = Math.max(adjustedWidth, 10); else if (index === 0) adjustedWidth = Math.max(adjustedWidth, 10); else if (index === 1) adjustedWidth = Math.max(adjustedWidth, 12); else adjustedWidth = Math.max(adjustedWidth, 8); return adjustedWidth; }); const table = new Table({ head, style: this.style, colAligns, colWidths: adjustedWidths, wordWrap: true, wrapOnWordBoundary: true }); for (const row of this.rows) if (this.isSeparatorRow(row)) continue; else { let processedRow = row.map((cell, index) => { if (index === 0 && this.dateFormatter != null && typeof cell === "string" && this.isDateString(cell)) return this.dateFormatter(cell); return cell; }); if (this.compactMode) processedRow = this.filterRowToCompact(processedRow, compactIndices); table.push(processedRow); } return table.toString(); } else { const table = new Table({ head, style: this.style, colAligns, colWidths: columnWidths, wordWrap: true, wrapOnWordBoundary: true }); for (const row of this.rows) if (this.isSeparatorRow(row)) continue; else { const processedRow = this.compactMode ? this.filterRowToCompact(row, compactIndices) : row; table.push(processedRow); } return table.toString(); } } /** * Checks if a row is a separator row (contains only empty cells or dashes) * @param row - Row to check * @returns True if the row is a separator */ isSeparatorRow(row) { return row.every((cell) => { if (typeof cell === "object" && cell != null && "content" in cell) return cell.content === "" || /^─+$/.test(cell.content); return typeof cell === "string" && (cell === "" || /^─+$/.test(cell)); }); } /** * Checks if a string matches the YYYY-MM-DD date format * @param text - String to check * @returns True if the string is a valid date format */ isDateString(text) { return /^\d{4}-\d{2}-\d{2}$/.test(text); } }; /** * Formats a number with locale-specific thousand separators * @param num - The number to format * @returns Formatted number string with commas as thousand separators */ function formatNumber(num) { return num.toLocaleString("en-US"); } /** * Formats a number as USD currency with dollar sign and 2 decimal places * @param amount - The amount to format * @returns Formatted currency string (e.g., "$12.34") */ function formatCurrency(amount) { return `$${amount.toFixed(2)}`; } /** * Formats Claude model names into a shorter, more readable format * Extracts model type and generation from full model name * @param modelName - Full model name (e.g., "claude-sonnet-4-20250514") * @returns Shortened model name (e.g., "sonnet-4") or original if pattern doesn't match */ function formatModelName(modelName) { const match = modelName.match(/claude-(\w+)-(\d+)-\d+/); if (match != null) return `${match[1]}-${match[2]}`; return modelName; } /** * Formats an array of model names for display as a comma-separated string * Removes duplicates and sorts alphabetically * @param models - Array of model names * @returns Formatted string with unique, sorted model names separated by commas */ function formatModelsDisplay(models) { const uniqueModels = uniq(models.map(formatModelName)); return uniqueModels.sort().join(", "); } /** * Formats an array of model names for display with each model on a new line * Removes duplicates and sorts alphabetically * @param models - Array of model names * @returns Formatted string with unique, sorted model names as a bulleted list */ function formatModelsDisplayMultiline(models) { const uniqueModels = uniq(models.map(formatModelName)); return uniqueModels.sort().map((model) => `- ${model}`).join("\n"); } /** * Pushes model breakdown rows to a table * @param table - The table to push rows to * @param table.push - Method to add rows to the table * @param breakdowns - Array of model breakdowns * @param extraColumns - Number of extra empty columns before the data (default: 1 for models column) * @param trailingColumns - Number of extra empty columns after the data (default: 0) */ function pushBreakdownRows(table, breakdowns, extraColumns = 1, trailingColumns = 0) { for (const breakdown of breakdowns) { const row = [` └─ ${formatModelName(breakdown.modelName)}`]; for (let i = 0; i < extraColumns; i++) row.push(""); const totalTokens = breakdown.inputTokens + breakdown.outputTokens + breakdown.cacheCreationTokens + breakdown.cacheReadTokens; row.push(pc.gray(formatNumber(breakdown.inputTokens)), pc.gray(formatNumber(breakdown.outputTokens)), pc.gray(formatNumber(breakdown.cacheCreationTokens)), pc.gray(formatNumber(breakdown.cacheReadTokens)), pc.gray(formatNumber(totalTokens)), pc.gray(formatCurrency(breakdown.cost))); for (let i = 0; i < trailingColumns; i++) row.push(""); table.push(row); } } /** * Manages live monitoring of Claude usage with efficient data reloading */ var LiveMonitor = class { config; fetcher = null; lastFileTimestamps = new Map(); processedHashes = new Set(); allEntries = []; constructor(config) { this.config = config; if (config.mode !== "display") this.fetcher = new PricingFetcher(); } /** * Implements Disposable interface */ [Symbol.dispose]() { this.fetcher?.[Symbol.dispose](); } /** * Gets the current active session block with minimal file reading * Only reads new or modified files since last check */ async getActiveBlock() { const results = await globUsageFiles(this.config.claudePaths); const allFiles = results.map((r) => r.file); if (allFiles.length === 0) return null; const filesToRead = []; for (const file of allFiles) { const timestamp = await getEarliestTimestamp(file); const lastTimestamp = this.lastFileTimestamps.get(file); if (timestamp != null && (lastTimestamp == null || timestamp.getTime() > lastTimestamp)) { filesToRead.push(file); this.lastFileTimestamps.set(file, timestamp.getTime()); } } if (filesToRead.length > 0) { const sortedFiles = await sortFilesByTimestamp(filesToRead); for (const file of sortedFiles) { const content = await readFile(file, "utf-8").catch(() => { return ""; }); const lines = content.trim().split("\n").filter((line) => line.length > 0); for (const line of lines) try { const parsed = JSON.parse(line); const result = usageDataSchema.safeParse(parsed); if (!result.success) continue; const data = result.data; const uniqueHash = createUniqueHash(data); if (uniqueHash != null && this.processedHashes.has(uniqueHash)) continue; if (uniqueHash != null) this.processedHashes.add(uniqueHash); const costUSD = await (this.config.mode === "display" ? Promise.resolve(data.costUSD ?? 0) : calculateCostForEntry(data, this.config.mode, this.fetcher)); const usageLimitResetTime = getUsageLimitResetTime(data); this.allEntries.push({ timestamp: new Date(data.timestamp), usage: { inputTokens: data.message.usage.input_tokens ?? 0, outputTokens: data.message.usage.output_tokens ?? 0, cacheCreationInputTokens: data.message.usage.cache_creation_input_tokens ?? 0, cacheReadInputTokens: data.message.usage.cache_read_input_tokens ?? 0 }, costUSD, model: data.message.model ?? "<synthetic>", version: data.version, usageLimitResetTime: usageLimitResetTime ?? void 0 }); } catch {} } } const blocks = identifySessionBlocks(this.allEntries, this.config.sessionDurationHours); const sortedBlocks = this.config.order === "asc" ? blocks : blocks.reverse(); return sortedBlocks.find((block) => block.isActive) ?? null; } /** * Clears all cached data to force a full reload */ clearCache() { this.lastFileTimestamps.clear(); this.processedHashes.clear(); this.allEntries = []; } }; const SYNC_START = "\x1B[?2026h"; const SYNC_END = "\x1B[?2026l"; const DISABLE_LINE_WRAP = "\x1B[?7l"; const ENABLE_LINE_WRAP = "\x1B[?7h"; const ANSI_RESET = "\x1B[0m"; /** * Manages terminal state for live updates * Provides a clean interface for terminal operations with automatic TTY checking * and cursor state management for live monitoring displays */ var TerminalManager = class { stream; cursorHidden = false; buffer = []; useBuffering = false; alternateScreenActive = false; syncMode = false; constructor(stream = process$1.stdout) { this.stream = stream; } /** * Hides the terminal cursor for cleaner live updates * Only works in TTY environments (real terminals) */ hideCursor() { if (!this.cursorHidden && this.stream.isTTY) { this.stream.write(ansiEscapes$2.cursorHide); this.cursorHidden = true; } } /** * Shows the terminal cursor * Should be called during cleanup to restore normal terminal behavior */ showCursor() { if (this.cursorHidden && this.stream.isTTY) { this.stream.write(ansiEscapes$2.cursorShow); this.cursorHidden = false; } } /** * Clears the entire screen and moves cursor to top-left corner * Essential for live monitoring displays that need to refresh completely */ clearScreen() { if (this.stream.isTTY) { this.stream.write(ansiEscapes$2.clearScreen); this.stream.write(ansiEscapes$2.cursorTo(0, 0)); } } /** * Writes text to the terminal stream * Supports buffering mode for performance optimization */ write(text) { if (this.useBuffering) this.buffer.push(text); else this.stream.write(text); } /** * Enables buffering mode - collects all writes in memory instead of sending immediately * This prevents flickering when doing many rapid updates */ startBuffering() { this.useBuffering = true; this.buffer = []; } /** * Sends all buffered content to terminal at once * This creates smooth, atomic updates without flickering */ flush() { if (this.useBuffering && this.buffer.length > 0) { if (this.syncMode && this.stream.isTTY) this.stream.write(SYNC_START + this.buffer.join("") + SYNC_END); else this.stream.write(this.buffer.join("")); this.buffer = []; } this.useBuffering = false; } /** * Switches to alternate screen buffer (like vim/less does) * This preserves what was on screen before and allows full-screen apps */ enterAlternateScreen() { if (!this.alternateScreenActive && this.stream.isTTY) { this.stream.write(ansiEscapes$2.enterAlternativeScreen); this.stream.write(DISABLE_LINE_WRAP); this.alternateScreenActive = true; } } /** * Returns to normal screen, restoring what was there before */ exitAlternateScreen() { if (this.alternateScreenActive && this.stream.isTTY) { this.stream.write(ENABLE_LINE_WRAP); this.stream.write(ansiEscapes$2.exitAlternativeScreen); this.alternateScreenActive = false; } } /** * Enables sync mode - terminal will wait for END signal before showing updates * Prevents the user from seeing partial/torn screen updates */ enableSyncMode() { this.syncMode = true; } /** * Disables synchronized output mode */ disableSyncMode() { this.syncMode = false; } /** * Gets terminal width in columns * Falls back to 80 columns if detection fails */ get width() { return this.stream.columns || 80; } /** * Gets terminal height in rows * Falls back to 24 rows if detection fails */ get height() { return this.stream.rows || 24; } /** * Returns true if output goes to a real terminal (not a file or pipe) * We only send fancy ANSI codes to real terminals */ get isTTY() { return this.stream.isTTY ?? false; } /** * Restores terminal to normal state - MUST call before program exits * Otherwise user's terminal might be left in a broken state */ cleanup() { this.showCursor(); this.exitAlternateScreen(); this.disableSyncMode(); } }; /** * Creates a progress bar string with customizable appearance * * Example: createProgressBar(75, 100, 20) -> "[████████████████░░░░] 75.0%" * * @param value - Current progress value * @param max - Maximum value (100% point) * @param width - Character width of the progress bar (excluding brackets and text) * @param options - Customization options for appearance and display * @param options.showPercentage - Whether to show percentage after the bar * @param options.showValues - Whether to show current/max values * @param options.fillChar - Character for filled portion (default: '█') * @param options.emptyChar - Character for empty portion (default: '░') * @param options.leftBracket - Left bracket character (default: '[') * @param options.rightBracket - Right bracket character (default: ']') * @param options.colors - Color configuration for different thresholds * @param options.colors.low - Color for low percentage values * @param options.colors.medium - Color for medium percentage values * @param options.colors.high - Color for high percentage values * @param options.colors.critical - Color for critical percentage values * @returns Formatted progress bar string with optional percentage/values */ function createProgressBar(value, max, width, options = {}) { const { showPercentage = true, showValues = false, fillChar = "█", emptyChar = "░", leftBracket = "[", rightBracket = "]", colors = {} } = options; const percentage = max > 0 ? Math.min(100, value / max * 100) : 0; const fillWidth = Math.round(percentage / 100 * width); const emptyWidth = width - fillWidth; let color = ""; if (colors.critical != null && percentage >= 90) color = colors.critical; else if (colors.high != null && percentage >= 80) color = colors.high; else if (colors.medium != null && percentage >= 50) color = colors.medium; else if (colors.low != null) color = colors.low; let bar = leftBracket; if (color !== "") bar += color; bar += fillChar.repeat(fillWidth); bar += emptyChar.repeat(emptyWidth); if (color !== "") bar += ANSI_RESET; bar += rightBracket; if (showPercentage) bar += ` ${percentage.toFixed(1)}%`; if (showValues) bar += ` (${value}/${max})`; return bar; } /** * Centers text within a specified width using spaces for padding * * Uses string-width to handle Unicode characters and ANSI escape codes properly. * If text is longer than width, returns original text without truncation. * * Example: centerText("Hello", 10) -> " Hello " * * @param text - Text to center (may contain ANSI color codes) * @param width - Total character width including padding * @returns Text with spaces added for centering */ function centerText(text, width) { const textLength = stringWidth(text); if (textLength >= width) return text; const leftPadding = Math.floor((width - textLength) / 2); const rightPadding = width - textLength - leftPadding; return " ".repeat(leftPadding) + text + " ".repeat(rightPadding); } /** * Simple delay function with abort signal support */ async function delay$1(ms, options) { return new Promise((resolve, reject) => { const timeout = setTimeout(resolve, ms); if (options?.signal) { if (options.signal.aborted) { clearTimeout(timeout); reject(new Error("Operation was aborted")); return; } options.signal.addEventListener("abort", () => { clearTimeout(timeout); reject(new Error("Operation was aborted")); }); } }); } /** * Get rate indicator (HIGH/MODERATE/NORMAL) based on burn rate */ function getRateIndicator(burnRate) { if (burnRate == null) return ""; switch (true) { case burnRate.tokensPerMinuteForIndicator > BURN_RATE_THRESHOLDS.HIGH: return pc.red("⚡ HIGH"); case burnRate.tokensPerMinuteForIndicator > BURN_RATE_THRESHOLDS.MODERATE: return pc.yellow("⚡ MODERATE"); default: return pc.green("✓ NORMAL"); } } /** * Delay with AbortSignal support and graceful error handling */ async function delayWithAbort$1(ms, signal) { await delay$1(ms, { signal }); } /** * Shows waiting message when no Claude session is active * Uses efficient cursor positioning instead of full screen clear */ async function renderWaitingState(terminal, config, signal) { terminal.startBuffering(); terminal.write(ansiEscapes$1.cursorTo(0, 0)); terminal.write(ansiEscapes$1.eraseDown); terminal.write(pc.yellow("No active session block found. Waiting...\n")); terminal.write(ansiEscapes$1.cursorHide); terminal.flush(); await delayWithAbort$1(config.refreshInterval, signal); } /** * Displays the live monitoring dashboard for active Claude session * Uses buffering and sync mode to prevent screen flickering */ function renderActiveBlock(terminal, activeBlock, config) { terminal.startBuffering(); terminal.write(ansiEscapes$1.cursorTo(0, 0)); terminal.write(ansiEscapes$1.eraseDown); renderLiveDisplay(terminal, activeBlock, config); terminal.write(ansiEscapes$1.cursorHide); terminal.flush(); } /** * Format token counts with K suffix for display */ function formatTokensShort$1(num) { if (num >= 1e3) return `${(num / 1e3).toFixed(1)}k`; return num.toString(); } /** * Renders the live display for an active session block */ function renderLiveDisplay(terminal, block, config) { const width = terminal.width; const now = new Date(); const totalTokens = getTotalTokens(block.tokenCounts); const elapsed = (now.getTime() - block.startTime.getTime()) / (1e3 * 60); const remaining = (block.endTime.getTime() - now.getTime()) / (1e3 * 60); const formatTokenDisplay = (tokens, useShort) => { return useShort ? formatTokensShort$1(tokens) : formatNumber(tokens); }; if (width < 60) { renderCompactLiveDisplay(terminal, block, config, totalTokens, elapsed, remaining); return; } const boxWidth = Math.min(120, width - 2); const boxMargin = Math.floor((width - boxWidth) / 2); const marginStr = " ".repeat(boxMargin); const sessionDuration = elapsed + remaining; const sessionPercent = elapsed / sessionDuration * 100; const sessionRightText = `${sessionPercent.toFixed(1).padStart(6)}%`; const tokenPercent = config.tokenLimit != null && config.tokenLimit > 0 ? totalTokens / config.tokenLimit * 100 : 0; const usageRightText = config.tokenLimit != null && config.tokenLimit > 0 ? `${tokenPercent.toFixed(1).padStart(6)}% (${formatTokensShort$1(totalTokens)}/${formatTokensShort$1(config.tokenLimit)})` : `(${formatTokensShort$1(totalTokens)} tokens)`; const projection = projectBlockUsage(block); const projectedPercent = projection != null && config.tokenLimit != null && config.tokenLimit > 0 ? projection.totalTokens / config.tokenLimit * 100 : 0; const projectionRightText = projection != null ? config.tokenLimit != null && config.tokenLimit > 0 ? `${projectedPercent.toFixed(1).padStart(6)}% (${formatTokensShort$1(projection.totalTokens)}/${formatTokensShort$1(config.tokenLimit)})` : `(${formatTokensShort$1(projection.totalTokens)} tokens)` : ""; const maxRightTextWidth = Math.max(stringWidth(sessionRightText), stringWidth(usageRightText), projection != null ? stringWidth(projectionRightText) : 0); const labelWidth = 14; const spacing = 4; const boxPadding = 4; const barWidth = boxWidth - labelWidth - maxRightTextWidth - spacing - boxPadding; const sessionProgressBar = createProgressBar(elapsed, sessionDuration, barWidth, { showPercentage: false, fillChar: pc.cyan("█"), emptyChar: pc.gray("░"), leftBracket: "[", rightBracket: "]" }); const startTime = block.startTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true }); const endTime = block.endTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true }); const detailsIndent = 3; const detailsSpacing = 2; const detailsAvailableWidth = boxWidth - 3 - detailsIndent; terminal.write(`${marginStr}┌${"─".repeat(boxWidth - 2)}┐\n`); terminal.write(`${marginStr}│${pc.bold(centerText("CLAUDE CODE - LIVE TOKEN USAGE MONITOR", boxWidth - 2))}│\n`); terminal.write(`${marginStr}├${"─".repeat(boxWidth - 2)}┤\n`); terminal.write(`${marginStr}│${" ".repeat(boxWidth - 2)}│\n`); const sessionLabel = pc.bold("⏱️ SESSION"); const sessionLabelWidth = stringWidth(sessionLabel); const sessionBarStr = `${sessionLabel}${"".padEnd(Math.max(0, labelWidth - sessionLabelWidth))} ${sessionProgressBar} ${sessionRightText}`; const sessionBarPadded = sessionBarStr + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(sessionBarStr))); terminal.write(`${marginStr}│ ${sessionBarPadded}│\n`); const sessionCol1 = `${pc.gray("Started:")} ${startTime}`; const sessionCol2 = `${pc.gray("Elapsed:")} ${prettyMs(elapsed * 60 * 1e3, { compact: true })}`; const sessionCol3 = `${pc.gray("Remaining:")} ${prettyMs(remaining * 60 * 1e3, { compact: true })} (${endTime})`; let sessionDetails = `${" ".repeat(detailsIndent)}${sessionCol1}${" ".repeat(detailsSpacing)}${sessionCol2}${" ".repeat(detailsSpacing)}${sessionCol3}`; const sessionDetailsWidth = stringWidth(sessionCol1) + stringWidth(sessionCol2) + stringWidth(sessionCol3) + detailsSpacing * 2; if (sessionDetailsWidth > detailsAvailableWidth) sessionDetails = `${" ".repeat(detailsIndent)}${sessionCol1}${" ".repeat(detailsSpacing)}${sessionCol3}`; const sessionDetailsPadded = sessionDetails + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(sessionDetails))); let usageLimitResetTimePadded = null; if (block.usageLimitResetTime !== void 0 && now < block.usageLimitResetTime) { const resetTime = block.usageLimitResetTime?.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: true }) ?? null; const usageLimitResetTime = resetTime !== null ? pc.red(`❌ USAGE LIMIT. RESET AT ${resetTime}`) : ""; usageLimitResetTimePadded = resetTime !== null ? usageLimitResetTime + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(usageLimitResetTime))) : null; } terminal.write(`${marginStr}│ ${sessionDetailsPadded}│\n`); if (usageLimitResetTimePadded !== null) terminal.write(`${marginStr}│ ${usageLimitResetTimePadded}│\n`); terminal.write(`${marginStr}│${" ".repeat(boxWidth - 2)}│\n`); terminal.write(`${marginStr}├${"─".repeat(boxWidth - 2)}┤\n`); terminal.write(`${marginStr}│${" ".repeat(boxWidth - 2)}│\n`); let barColor = pc.green; if (tokenPercent > 100) barColor = pc.red; else if (tokenPercent > 80) barColor = pc.yellow; const usageBar = config.tokenLimit != null && config.tokenLimit > 0 ? createProgressBar(totalTokens, config.tokenLimit, barWidth, { showPercentage: false, fillChar: barColor("█"), emptyChar: pc.gray("░"), leftBracket: "[", rightBracket: "]" }) : `[${pc.green("█".repeat(Math.floor(barWidth * .1)))}${pc.gray("░".repeat(barWidth - Math.floor(barWidth * .1)))}]`; const burnRate = calculateBurnRate(block); const rateIndicator = getRateIndicator(burnRate); const buildRateDisplay = (useShort) => { if (burnRate == null) return `${pc.bold("Burn Rate:")} N/A`; const rateValue = Math.round(burnRate.tokensPerMinute); const formattedRate = useShort ? formatTokensShort$1(rateValue) : formatNumber(rateValue); return `${pc.bold("Burn Rate:")} ${formattedRate} token/min ${rateIndicator}`; }; const usageLabel = pc.bold("🔥 USAGE"); const usageLabelWidth = stringWidth(usageLabel); const usageBarStr = `${usageLabel}${"".padEnd(Math.max(0, labelWidth - usageLabelWidth))} ${usageBar} ${usageRightText}`; let rateDisplay = buildRateDisplay(false); let usageCol1 = `${pc.gray("Tokens:")} ${formatTokenDisplay(totalTokens, false)} (${rateDisplay})`; let usageCol2 = config.tokenLimit != null && config.tokenLimit > 0 ? `${pc.gray("Limit:")} ${formatTokenDisplay(config.tokenLimit, false)} tokens` : ""; const usageCol3 = `${pc.gray("Cost:")} ${formatCurrency(block.costUSD)}`; let totalWidth = stringWidth(usageCol1); if (usageCol2.length > 0) totalWidth += detailsSpacing + stringWidth(usageCol2); totalWidth += detailsSpacing + stringWidth(usageCol3); let useTwoLineLayout = false; if (totalWidth > detailsAvailableWidth) { useTwoLineLayout = true; rateDisplay = buildRateDisplay(true); usageCol1 = `${pc.gray("Tokens:")} ${formatTokenDisplay(totalTokens, true)} (${rateDisplay})`; if (usageCol2.length > 0) usageCol2 = `${pc.gray("Limit:")} ${formatTokenDisplay(config.tokenLimit, true)} tokens`; } const usageBarPadded = usageBarStr + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(usageBarStr))); terminal.write(`${marginStr}│ ${usageBarPadded}│\n`); if (useTwoLineLayout) { const usageDetailsLine1 = `${" ".repeat(detailsIndent)}${usageCol1}`; const usageDetailsLine1Padded = usageDetailsLine1 + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(usageDetailsLine1))); terminal.write(`${marginStr}│ ${usageDetailsLine1Padded}│\n`); let usageDetailsLine2; if (usageCol2.length > 0) usageDetailsLine2 = `${" ".repeat(detailsIndent)}${usageCol2}${" ".repeat(detailsSpacing)}${usageCol3}`; else usageDetailsLine2 = `${" ".repeat(detailsIndent)}${usageCol3}`; const usageDetailsLine2Padded = usageDetailsLine2 + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(usageDetailsLine2))); terminal.write(`${marginStr}│ ${usageDetailsLine2Padded}│\n`); } else { let usageDetails; if (usageCol2.length > 0) usageDetails = `${" ".repeat(detailsIndent)}${usageCol1}${" ".repeat(detailsSpacing)}${usageCol2}${" ".repeat(detailsSpacing)}${usageCol3}`; else usageDetails = `${" ".repeat(detailsIndent)}${usageCol1}${" ".repeat(detailsSpacing)}${usageCol3}`; const usageDetailsPadded = usageDetails + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(usageDetails))); terminal.write(`${marginStr}│ ${usageDetailsPadded}│\n`); } terminal.write(`${marginStr}│${" ".repeat(boxWidth - 2)}│\n`); terminal.write(`${marginStr}├${"─".repeat(boxWidth - 2)}┤\n`); terminal.write(`${marginStr}│${" ".repeat(boxWidth - 2)}│\n`); if (projection != null) { let projBarColor = pc.green; if (projectedPercent > 100) projBarColor = pc.red; else if (projectedPercent > 80) projBarColor = pc.yellow; const projectionBar = config.tokenLimit != null && config.tokenLimit > 0 ? createProgressBar(projection.totalTokens, config.tokenLimit, barWidth, { showPercentage: false, fillChar: projBarColor("█"), emptyChar: pc.gray("░"), leftBracket: "[", rightBracket: "]" }) : `[${pc.green("█".repeat(Math.floor(barWidth * .15)))}${pc.gray("░".repeat(barWidth - Math.floor(barWidth * .15)))}]`; const limitStatus = config.tokenLimit != null && config.tokenLimit > 0 ? projectedPercent > 100 ? pc.red("❌ WILL EXCEED LIMIT") : projectedPercent > 80 ? pc.yellow("⚠️ APPROACHING LIMIT") : pc.green("✓ WITHIN LIMIT") : pc.green("✓ ON TRACK"); const projLabel = pc.bold("📈 PROJECTION"); const projLabelWidth = stringWidth(projLabel); const projBarStr = `${projLabel}${"".padEnd(Math.max(0, labelWidth - projLabelWidth))} ${projectionBar} ${projectionRightText}`; const projBarPadded = projBarStr + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(projBarStr))); terminal.write(`${marginStr}│ ${projBarPadded}│\n`); const projCol1 = `${pc.gray("Status:")} ${limitStatus}`; let projCol2 = `${pc.gray("Tokens:")} ${formatTokenDisplay(projection.totalTokens, false)}`; const projCol3 = `${pc.gray("Cost:")} ${formatCurrency(projection.totalCost)}`; const projTotalWidth = stringWidth(projCol1) + stringWidth(projCol2) + stringWidth(projCol3) + detailsSpacing * 2; let projUseTwoLineLayout = false; if (projTotalWidth > detailsAvailableWidth) { projUseTwoLineLayout = true; projCol2 = `${pc.gray("Tokens:")} ${formatTokenDisplay(projection.totalTokens, true)}`; } if (projUseTwoLineLayout) { const projDetailsLine1 = `${" ".repeat(detailsIndent)}${projCol1}`; const projDetailsLine1Padded = projDetailsLine1 + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(projDetailsLine1))); terminal.write(`${marginStr}│ ${projDetailsLine1Padded}│\n`); const projDetailsLine2 = `${" ".repeat(detailsIndent)}${projCol2}${" ".repeat(detailsSpacing)}${projCol3}`; const projDetailsLine2Padded = projDetailsLine2 + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(projDetailsLine2))); terminal.write(`${marginStr}│ ${projDetailsLine2Padded}│\n`); } else { const projDetails = `${" ".repeat(detailsIndent)}${projCol1}${" ".repeat(detailsSpacing)}${projCol2}${" ".repeat(detailsSpacing)}${projCol3}`; const projDetailsPadded = projDetails + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(projDetails))); terminal.write(`${marginStr}│ ${projDetailsPadded}│\n`); } terminal.write(`${marginStr}│${" ".repeat(boxWidth - 2)}│\n`); } if (block.models.length > 0) { terminal.write(`${marginStr}├${"─".repeat(boxWidth - 2)}┤\n`); const modelsLine = `⚙️ Models: ${formatModelsDisplay(block.models)}`; const modelsLinePadded = modelsLine + " ".repeat(Math.max(0, boxWidth - 3 - stringWidth(modelsLine))); terminal.write(`${marginStr}│ ${modelsLinePadded}│\n`); } terminal.write(`${marginStr}├${"─".repeat(boxWidth - 2)}┤\n`); const refreshText = `↻ Refreshing every ${config.refreshInterval / 1e3}s • Press Ctrl+C to stop`; terminal.write(`${marginStr}│${pc.gray(centerText(refreshText, boxWidth - 2))}│\n`); terminal.write(`${marginStr}└${"─".repeat(boxWidth - 2)}┘\n`); } /** * Renders a compact live display for narrow terminals */ function renderCompactLiveDisplay(terminal, block, config, totalTokens, elapsed, remaining) { const width = terminal.width; terminal.write(`${pc.bold(centerText("LIVE MONITOR", width))}\n`); terminal.write(`${"─".repeat(width)}\n`); const sessionPercent = elapsed / (elapsed + remaining) * 100; terminal.write(`Session: ${sessionPercent.toFixed(1)}% (${Math.floor(elapsed / 60)}h ${Math.floor(elapsed % 60)}m)\n`); if (config.tokenLimit != null && config.tokenLimit > 0) { const tokenPercent = totalTokens / config.tokenLimit * 100; const status = tokenPercent > 100 ? pc.red("OVER") : tokenPercent > 80 ? pc.yellow("WARN") : pc.green("OK"); terminal.write(`Tokens: ${formatNumber(totalTokens)}/${formatNumber(config.tokenLimit)} ${status}\n`); } else terminal.write(`Tokens: ${formatNumber(totalTokens)}\n`); terminal.write(`Cost: ${formatCurrency(block.costUSD)}\n`); const burnRate = calculateBurnRate(block); if (burnRate != null) terminal.write(`Rate: ${formatNumber(burnRate.tokensPerMinute)}/min\n`); terminal.write(`${"─".repeat(width)}\n`); terminal.write(pc.gray(`Refresh: ${config.refreshInterval / 1e3}s | Ctrl+C: stop\n`)); } var import_usingCtx$2 = __toESM(require_usingCtx(), 1); async function startLiveMonitoring(config) { try { var _usingCtx = (0, import_usingCtx$2.default)(); const terminal = new TerminalManager(); const abortController = new AbortController(); let lastRenderTime = 0; const cleanup = () => { abortController.abort(); terminal.cleanup(); terminal.clearScreen(); logger.info("Live monitoring stopped."); if (process$1.exitCode == null) process$1.exit(0); }; process$1.on("SIGINT", cleanup); process$1.on("SIGTERM", cleanup); terminal.enterAlternateScreen(); terminal.enableSyncMode(); terminal.clearScreen(); terminal.hideCursor(); const monitor = _usingCtx.u(new LiveMonitor({ claudePaths: config.claudePaths, sessionDurationHours: config.sessionDurationHours, mode: config.mode, order: config.order })); const monitoringResult = await Result.try({ try: async () => { while (!abortController.signal.aborted) { const now = Date.now(); const timeSinceLastRender = now - lastRenderTime; if (timeSinceLastRender < MIN_RENDER_INTERVAL_MS) { await delayWithAbort$1(MIN_RENDER_INTERVAL_MS - timeSinceLastRender, abortController.signal); continue; } const activeBlock = await monitor.getActiveBlock(); monitor.clearCache(); if (activeBlock == null) { await renderWaitingState(terminal, config, abortController.signal); continue; } renderActiveBlock(terminal, activeBlock, config); lastRenderTime = Date.now(); let resizeEventHandler; try { await Promise.race([delayWithAbort$1(config.refreshInterval, abortController.signal), new Promise((resolve) => { resizeEventHandler = resolve; process$1.stdout.once("resize", resolve); })]); } finally { if (resizeEventHandler != null) process$1.stdout.removeListener("resize", resizeEventHandler); } } }, catch: (error) => error })(); if (Result.isFailure(monitoringResult)) { const error = monitoringResult.error; if ((error instanceof DOMException || error instanceof Error) && error.name === "AbortError") return; const errorMessage = error instanceof Error ? error.message : String(error); terminal.startBuffering(); terminal.clearScreen(); terminal.write(pc.red(`Error: ${errorMessage}\n`)); terminal.flush(); logger.error(`Live monitoring error: ${errorMessage}`); await delayWithAbort$1(config.refreshInterval, abortController.signal).catch(() => {}); } } catch (_) { _usingCtx.e = _; } finally { _usingCtx.d(); } } /** * Formats the time display for a session block * @param block - Session block to format * @param compact - Whether to use compact formatting for narrow terminals * @returns Formatted time string with duration and status information */ function formatBlockTime(block, compact = false) { const start = compact ? block.startTime.toLocaleString(void 0, { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : block.startTime.toLocaleString(); if (block.isGap ?? false) { const end = compact ? block.endTime.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit" }) : block.endTime.toLocaleString(); const duration$1 = Math.round((block.endTime.getTime() - block.startTime.getTime()) / (1e3 * 60 * 60)); return compact ? `${start}-${end}\n(${duration$1}h gap)` : `${start} - ${end} (${duration$1}h gap)`; } const duration = block.actualEndTime != null ? Math.round((block.actualEndTime.getTime() - block.startTime.getTime()) / (1e3 * 60)) : 0; if (block.isActive) { const now = new Date(); const elapsed = Math.round((now.getTime() - block.startTime.getTime()) / (1e3 * 60)); const remaining = Math.round((block.endTime.getTime() - now.getTime()) / (1e3 * 60)); const elapsedHours = Math.floor(elapsed / 60); const elapsedMins = elapsed % 60; const remainingHours = Math.floor(remaining / 60); const remainingMins = remaining % 60; if (compact) return `${start}\n(${elapsedHours}h${elapsedMins}m/${remainingHours}h${remainingMins}m)`; return `${start} (${elapsedHours}h ${elapsedMins}m elapsed, ${remainingHours}h ${remainingMins}m remaining)`; } const hours = Math.floor(duration / 60); const mins = duration % 60; if (compact) return hours > 0 ? `${start}\n(${hours}h${mins}m)` : `${start}\n(${mins}m)`; if (hours > 0) return `${start} (${hours}h ${mins}m)`; return `${start} (${mins}m)`; } /** * Formats the list of models used in a block for display * @param models - Array of model names * @returns Formatted model names string */ function formatModels(models) { if (models.length === 0) return "-"; return formatModelsDisplayMultiline(models); } /** * Parses token limit argument, supporting 'max' keyword * @param value - Token limit string value * @param maxFromAll - Maximum token count found in all blocks * @returns Parsed token limit or undefined if invalid */ function parseTokenLimit(value, maxFromAll) { if (value == null || value === "" || value === "max") return maxFromAll > 0 ? maxFromAll : void 0; const limit = Number.parseInt(value, 10); return Number.isNaN(limit) ? void 0 : limit; } const blocksCommand = define({ name: "blocks", description: "Show usage report grouped by session billing blocks", args: { ...sharedCommandConfig.args, active: { type: "boolean", short: "a", description: "Show only active block with projections", default: false }, recent: { type: "boolean", short: "r", description: `Show blocks from last ${DEFAULT_RECENT_DAYS} days (including active)`, default: false }, tokenLimit: { type: "string", short: "t", description: "Token limit for quota warnings (e.g., 500000 or \"max\")" }, sessionLength: { type: "number", short: "l", description: `Session block duration in hours (default: ${DEFAULT_SESSION_DURATION_HOURS})`, default: DEFAULT_SESSION_DURATION_HOURS }, live: { type: "boolean", description: "Live monitoring mode with real-time updates", default: false }, refreshInterval: { type: "number", description: `Refresh interval in seconds for live mode (default: ${DEFAULT_REFRESH_INTERVAL_SECONDS})`, default: DEFAULT_REFRESH_INTERVAL_SECONDS } }, toKebab: true, async run(ctx) { if (ctx.values.json) logger.level = 0; if (ctx.values.sessionLength <= 0) { logger.error("Session length must be a positive number"); process$1.exit(1); } let blocks = await loadSessionBlockData({ since: ctx.values.since, until: ctx.values.until, mode: ctx.values.mode, order: ctx.values.order, offline: ctx.values.offline, sessionDurationHours: ctx.values.sessionLength }); if (blocks.length === 0) { if (ctx.values.json) log(JSON.stringify({ blocks: [] })); else logger.warn("No Claude usage data found."); process$1.exit(0); } let maxTokensFromAll = 0; if (ctx.values.tokenLimit === "max" || ctx.values.tokenLimit == null || ctx.values.tokenLimit === "") { for (const block of blocks) if (!(block.isGap ?? false) && !block.isActive) { const blockTokens = getTotalTokens(block.tokenCounts); if (blockTokens > maxTokensFromAll) maxTokensFromAll = blockTokens; } if (!ctx.values.json && maxTokensFromAll > 0) logger.info(`Using max tokens from previous sessions: ${formatNumber(maxTokensFromAll)}`); } if (ctx.values.recent) blocks = filterRecentBlocks(blocks, DEFAULT_RECENT_DAYS); if (ctx.values.active) { blocks = blocks.filter((block) => block.isActive); if (blocks.length === 0) { if (ctx.values.json) log(JSON.stringify({ blocks: [], message: "No active block" })); else logger.info("No active session block found."); process$1.exit(0); } } if (ctx.values.live && !ctx.values.json) { if (!ctx.values.active) logger.info("Live mode automatically shows only active blocks."); let tokenLimitValue = ctx.values.tokenLimit; if (tokenLimitValue == null || tokenLimitValue === "") { tokenLimitValue = "max"; if (maxTokensFromAll > 0) logger.info(`No token limit specified, using max from previous sessions: ${formatNumber(maxTokensFromAll)}`); } const refreshInterval = Math.max(MIN_REFRESH_INTERVAL_SECONDS, Math.min(MAX_REFRESH_INTERVAL_SECONDS, ctx.values.refreshInterval)); if (refreshInterval !== ctx.values.refreshInterval) logger.warn(`Refresh interval adjusted to ${refreshInterval} seconds (valid range: ${MIN_REFRESH_INTERVAL_SECONDS}-${MAX_REFRESH_INTERVAL_SECONDS})`); const paths = getClaudePaths(); if (paths.length === 0) { logger.error("No valid Claude data directory found"); throw new Error("No valid Claude data directory found"); } await startLiveMonitoring({ claudePaths: paths, tokenLimit: parseTokenLimit(tokenLimitValue, maxTokensFromAll), refreshInterval: refreshInterval * 1e3, sessionDurationHours: ctx.values.sessionLength, mode: ctx.values.mode, order: ctx.values.order }); return; } if (ctx.values.json) { const jsonOutput = { blocks: blocks.map((block) => { const burnRate = block.isActive ? calculateBurnRate(block) : null; const projection = block.isActive ? projectBlockUsage(block) : null; return { id: block.id, startTime: block.startTime.toISOString(), endTime: block.endTime.toISOString(), actualEndTime: block.actualEndTime?.toISOString() ?? null, isActive: block.isActive, isGap: block.isGap ?? false, entries: block.entries.length, tokenCounts: block.tokenCounts, totalTokens: getTotalTokens(block.tokenCounts), costUSD: block.costUSD, models: block.models, burnRate, projection, tokenLimitStatus: projection != null && ctx.values.tokenLimit != null ? (() => { const limit = parseTokenLimit(ctx.values.tokenLimit, maxTokensFromAll); return limit != null ? { limit, projectedUsage: projection.totalTokens, percentUsed: projection.totalTokens / limit * 100, status: projection.totalTokens > limit ? "exceeds" : projection.totalTokens > limit * BLOCKS_WARNING_THRESHOLD ? "warning" : "ok" } : void 0; })() : void 0, usageLimitResetTime: block.usageLimitResetTime }; }) }; log(JSON.stringify(jsonOutput, null, 2)); } else if (ctx.values.active && blocks.length === 1) { const block = blocks[0]; if (block == null) { logger.warn("No active block found."); process$1.exit(0); } const burnRate = calculateBurnRate(block); const projection = projectBlockUsage(block); logger.box("Current Session Block Status"); const now = new Date(); const elapsed = Math.round((now.getTime() - block.startTime.getTime()) / (1e3 * 60)); const remaining = Math.round((block.endTime.getTime() - now.getTime()) / (1e3 * 60)); log(`Block Started: ${pc.cyan(block.startTime.toLocaleString())} (${pc.yellow(`${Math.floor(elapsed / 60)}h ${elapsed % 60}m`)} ago)`); log(`Time Remaining: ${pc.green(`${Math.floor(remaining / 60)}h ${remaining % 60}m`)}\n`); log(pc.bold("Current Usage:")); log(` Input Tokens: ${formatNumber(block.tokenCounts.inputTokens)}`); log(` Output Tokens: ${formatNumber(block.tokenCounts.outputTokens)}`); log(` Total Cost: ${formatCurrency(block.costUSD)}\n`); if (burnRate != null) { log(pc.bold("Burn Rate:")); log(` Tokens/minute: ${formatNumber(burnRate.tok