ccusage-live
Version:
Enhanced Claude Code usage analysis tool with live team monitoring and collaboration features
1,209 lines • 171 kB
JavaScript
#!/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