ccusage-fleet
Advanced tools
+65
| function number(value) { | ||
| return typeof value === 'number' && Number.isFinite(value) ? value : 0; | ||
| } | ||
| function compact(value, prefix = '') { | ||
| const numeric = number(value); | ||
| const absolute = Math.abs(numeric); | ||
| if (absolute >= 1_000_000_000) return `${prefix}${(numeric / 1_000_000_000).toFixed(1)}B`; | ||
| if (absolute >= 1_000_000) return `${prefix}${(numeric / 1_000_000).toFixed(1)}M`; | ||
| if (absolute >= 1_000) return `${prefix}${(numeric / 1_000).toFixed(1)}K`; | ||
| return `${prefix}${Math.round(numeric)}`; | ||
| } | ||
| function bar(value, peak, width) { | ||
| if (value <= 0 || peak <= 0) return ' '.repeat(width); | ||
| const scaled = Math.min(width, (value / peak) * width); | ||
| let full = Math.floor(scaled); | ||
| const fraction = scaled - full; | ||
| const partials = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']; | ||
| const partialIndex = Math.round(fraction * 8); | ||
| if (partialIndex === 8) { | ||
| full += 1; | ||
| } | ||
| const partial = full < width ? partials[partialIndex] ?? '' : ''; | ||
| const used = Math.min(width, full + (partial ? 1 : 0)); | ||
| return `${'█'.repeat(Math.min(full, width))}${partial}${'░'.repeat(width - used)}`; | ||
| } | ||
| function pad(value, width, right = false) { | ||
| const text = String(value); | ||
| return right ? text.padStart(width) : text.padEnd(width); | ||
| } | ||
| function border(left, middle, right, widths, fill = '─') { | ||
| return `${left}${widths.map((width) => fill.repeat(width + 2)).join(middle)}${right}`; | ||
| } | ||
| export function renderFleetGraph(fleet, settings, terminalWidth = process.stdout.columns ?? 120) { | ||
| const rows = fleet[fleet.command]; | ||
| const bucketName = fleet.command === 'weekly' ? 'week' : fleet.command === 'monthly' ? 'month' : 'day'; | ||
| const metricKey = settings.graphMetric === 'cost' ? 'totalCost' : 'totalTokens'; | ||
| const values = rows.map((row) => number(row[metricKey])); | ||
| const total = values.reduce((sum, value) => sum + value, 0); | ||
| const peak = Math.max(0, ...values); | ||
| const metricTitle = settings.graphMetric === 'cost' ? 'Cost' : 'Total tokens'; | ||
| const metricValue = (value) => settings.graphMetric === 'cost' ? compact(value, '$') : compact(value); | ||
| const title = `${metricTitle} over time · ${bucketName} buckets · total ${metricValue(total)} · peak ${metricValue(peak)}`; | ||
| const widths = [10, 10, 10, Math.max(16, Math.min(60, terminalWidth - 46))]; | ||
| const output = [title, border('╭', '┬', '╮', widths)]; | ||
| const headers = ['BUCKET', 'TOKENS', 'COST', 'DISTRIBUTION']; | ||
| output.push(`│ ${headers.map((header, index) => pad(header, widths[index])).join(' ┆ ')} │`); | ||
| output.push(border('╞', '╪', '╡', widths, '═')); | ||
| for (const row of rows) { | ||
| const cells = [ | ||
| pad(row.period, widths[0]), | ||
| pad(compact(row.totalTokens), widths[1], true), | ||
| pad(settings.noCost ? '—' : compact(row.totalCost, '$'), widths[2], true), | ||
| bar(number(row[metricKey]), peak, widths[3]), | ||
| ]; | ||
| output.push(`│ ${cells.join(' ┆ ')} │`); | ||
| } | ||
| output.push(border('╰', '┴', '╯', widths)); | ||
| return output.join('\n'); | ||
| } |
@@ -29,2 +29,4 @@ { | ||
| "groupBy": { "enum": ["agent", "device", "none"] }, | ||
| "graph": { "type": "boolean" }, | ||
| "graphMetric": { "enum": ["tokens", "cost"] }, | ||
| "byHost": { "type": "boolean" }, | ||
@@ -31,0 +33,0 @@ "offline": { "type": "boolean" }, |
+1
-1
| { | ||
| "name": "ccusage-fleet", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "description": "Aggregate ccusage reports across local and SSH-connected machines", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+14
-0
@@ -41,2 +41,8 @@ # ccusage-fleet | ||
| # Append a report-bucket token distribution graph | ||
| npx ccusage-fleet daily --hosts localhost,rtzr --graph | ||
| # Scale the graph by estimated cost | ||
| npx ccusage-fleet daily --hosts localhost,rtzr --graph --graph-metric cost | ||
| # One consistent date range and timezone on every device | ||
@@ -70,2 +76,4 @@ npx ccusage-fleet daily \ | ||
| "groupBy": "agent", | ||
| "graph": false, | ||
| "graphMetric": "tokens", | ||
| "concurrency": 4, | ||
@@ -96,2 +104,8 @@ "timeoutMs": 120000 | ||
| ## Graph | ||
| Add `--graph` to append a proportional distribution chart after the table. Bars use total tokens by default; choose `--graph-metric cost` to scale them by estimated cost. Buckets follow the report command: days for `daily`, weeks for `weekly`, and months for `monthly`. | ||
| Hour buckets will require a future normalized event export from ccusage; ccusage-fleet does not copy raw transcripts to approximate them. | ||
| ## Failure handling | ||
@@ -98,0 +112,0 @@ |
+5
-0
@@ -13,2 +13,3 @@ const VALUE_OPTIONS = new Map([ | ||
| ['--group-by', 'groupBy'], | ||
| ['--graph-metric', 'graphMetric'], | ||
| ]); | ||
@@ -26,2 +27,4 @@ | ||
| ['--dry-run', ['dryRun', true]], | ||
| ['--graph', ['graph', true]], | ||
| ['--no-graph', ['graph', false]], | ||
| ['--help', ['help', true]], | ||
@@ -108,2 +111,4 @@ ['-h', ['help', true]], | ||
| --by-host / --no-by-host Compatibility aliases for --group-by device/none | ||
| --graph / --no-graph Show a report-bucket distribution graph | ||
| --graph-metric <metric> Scale graph bars by tokens or cost (default: tokens) | ||
| --no-cost Hide costs | ||
@@ -110,0 +115,0 @@ --offline / --online Use bundled or online ccusage pricing (default: offline) |
+6
-1
@@ -10,2 +10,3 @@ import { readFileSync } from 'node:fs'; | ||
| import { renderFleetTable } from './table.js'; | ||
| import { renderFleetGraph } from './graph.js'; | ||
@@ -66,5 +67,9 @@ const here = dirname(fileURLToPath(import.meta.url)); | ||
| } else { | ||
| process.stdout.write(`${renderFleetTable(fleet, settings)}\n`); | ||
| const sections = [renderFleetTable(fleet, settings)]; | ||
| if (settings.graph) { | ||
| sections.push(renderFleetGraph(fleet, settings)); | ||
| } | ||
| process.stdout.write(`${sections.join('\n\n')}\n`); | ||
| } | ||
| return settings.strict && successful.length !== results.length ? 1 : 0; | ||
| } |
+11
-1
@@ -146,2 +146,10 @@ import { existsSync, readFileSync } from 'node:fs'; | ||
| } | ||
| const graphMetric = parsedOptions.graphMetric ?? config.graphMetric ?? 'tokens'; | ||
| if (!['tokens', 'cost'].includes(graphMetric)) { | ||
| throw new Error(`graph-metric must be tokens or cost (received '${graphMetric}')`); | ||
| } | ||
| const noCost = parsedOptions.noCost ?? config.noCost ?? false; | ||
| if (graphMetric === 'cost' && noCost) { | ||
| throw new Error('graph-metric cost cannot be combined with --no-cost'); | ||
| } | ||
@@ -154,5 +162,7 @@ return { | ||
| groupBy, | ||
| graph: parsedOptions.graph ?? config.graph ?? false, | ||
| graphMetric, | ||
| hosts, | ||
| json: parsedOptions.json ?? false, | ||
| noCost: parsedOptions.noCost ?? config.noCost ?? false, | ||
| noCost, | ||
| offline: parsedOptions.offline ?? config.offline ?? true, | ||
@@ -159,0 +169,0 @@ since: parsedOptions.since, |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
40800
12.85%12
9.09%925
9.6%129
12.17%