@koinunopochi/ccusage-to-graph
Version:
Display ccusage JSON output as terminal graphs with Claude Pro/Pro Max threshold indicators
223 lines • 9.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = require("commander");
const asciichart = __importStar(require("asciichart"));
const chalk_1 = __importDefault(require("chalk"));
// CLI設定
commander_1.program
.version('0.2.1')
.description('Display ccusage JSON output as terminal graphs')
.option('-t, --type <type>', 'graph type (bar, line)', 'bar')
.option('-p, --period <period>', 'time period to display (day, week, month)', 'day')
.option('--no-threshold', 'hide Pro Max threshold lines')
.parse(process.argv);
const options = commander_1.program.opts();
// 標準入力からJSONを読み取る
let inputData = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
inputData += chunk;
});
process.stdin.on('end', () => {
try {
const jsonData = JSON.parse(inputData);
displayGraph(jsonData, options);
}
catch (error) {
console.error(chalk_1.default.red('Error parsing JSON input:'), error.message);
process.exit(1);
}
});
process.stdin.on('error', (error) => {
console.error(chalk_1.default.red('Error reading input:'), error.message);
process.exit(1);
});
// タイムアウト設定(10秒)
setTimeout(() => {
if (inputData === '') {
console.error(chalk_1.default.yellow('No input received. Usage: ccusage --json | ccusage-graph'));
process.exit(1);
}
}, 10000);
// グラフ表示関数
function displayGraph(data, options) {
// ccusageの新しいフォーマット(daily)と旧フォーマット(usage)の両方に対応
let usageData = [];
if (data.daily && data.daily.length > 0) {
// 新フォーマット
usageData = data.daily.map(item => ({
date: item.date,
cost: item.totalCost,
tokens: item.totalTokens
}));
}
else if (data.usage && data.usage.length > 0) {
// 旧フォーマット(テストデータ用)
usageData = data.usage.map(item => ({
date: item.date,
cost: item.cost,
tokens: item.tokens
}));
}
else {
console.error(chalk_1.default.red('No usage data found in JSON'));
process.exit(1);
}
// 日付でソート
usageData.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
// 最高値を見つける
const maxCost = Math.max(...usageData.map(item => item.cost));
console.log(chalk_1.default.bold.cyan('\n📊 Claude Usage Report\n'));
if (options.type === 'line') {
// 折れ線グラフ表示
const costs = usageData.map(item => item.cost);
const chart = asciichart.plot(costs, {
height: 15,
padding: ' ',
format: (x) => x.toFixed(2).padStart(8)
});
console.log(chalk_1.default.green(chart));
// X軸ラベル表示
console.log('\n' + chalk_1.default.dim('Dates:'));
usageData.forEach((item, index) => {
const date = new Date(item.date);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const label = `${month}/${day}`;
const crown = item.cost === maxCost ? ' 👑' : '';
console.log(chalk_1.default.dim(` [${index}] ${label}: $${item.cost.toFixed(2)}${crown}`));
});
}
else {
// 棒グラフ表示(ASCII文字で手動実装)
displayBarChart(usageData, options, maxCost);
}
// 合計表示
if (data.total) {
console.log(chalk_1.default.bold.yellow(`\n💰 Total Cost: $${data.total.cost.toFixed(2)}`));
console.log(chalk_1.default.bold.blue(`🔢 Total Tokens: ${data.total.tokens.toLocaleString()}`));
}
else {
// daily形式の場合は自分で計算
const totalCost = usageData.reduce((sum, item) => sum + item.cost, 0);
const totalTokens = usageData.reduce((sum, item) => sum + (item.tokens || 0), 0);
console.log(chalk_1.default.bold.yellow(`\n💰 Total Cost: $${totalCost.toFixed(2)}`));
if (totalTokens > 0) {
console.log(chalk_1.default.bold.blue(`🔢 Total Tokens: ${totalTokens.toLocaleString()}`));
}
}
}
// 棒グラフ表示関数
function displayBarChart(usageData, options, maxCostValue) {
const maxCost = maxCostValue || Math.max(...usageData.map(item => item.cost));
const chartWidth = 50;
const proMax20 = 20;
const proMax200 = 200;
// スケールを適切に設定
// スケールを固定値(例:$50, $100, $500)に設定することで、しきい値の位置を一定に保つ
let scale;
if (maxCost <= 50) {
scale = 50;
}
else if (maxCost <= 100) {
scale = 100;
}
else if (maxCost <= 200) {
scale = 200;
}
else if (maxCost <= 500) {
scale = 500;
}
else {
scale = Math.ceil(maxCost / 100) * 100; // 100単位で切り上げ
}
usageData.forEach(item => {
const date = new Date(item.date);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateLabel = `${month}/${day}`;
const crown = item.cost === maxCost ? ' 👑' : ' ';
const label = `${dateLabel}${crown}`.padEnd(10);
// バーの長さは切り上げて、わずかでも超えた場合は次の文字を表示
const barLength = Math.ceil((item.cost / scale) * chartWidth);
// バーを構築
let displayBar = '';
// $20と$200のマーカー位置を計算(こちらは切り捨てて、確実に超えた時のみマーカーを移動)
const marker20Pos = Math.floor((proMax20 / scale) * chartWidth);
const marker200Pos = Math.floor((proMax200 / scale) * chartWidth);
for (let i = 0; i < chartWidth; i++) {
if (i < barLength) {
// バーの部分
if (item.cost >= proMax200) {
displayBar += chalk_1.default.red('█');
}
else if (item.cost >= proMax20) {
displayBar += chalk_1.default.yellow('█');
}
else {
displayBar += chalk_1.default.green('█');
}
}
else if (options.threshold && i === marker20Pos && scale >= proMax20) {
displayBar += chalk_1.default.dim.yellow('|');
}
else if (options.threshold && i === marker200Pos && scale >= proMax200) {
displayBar += chalk_1.default.dim.red('|');
}
else {
displayBar += ' ';
}
}
console.log(`${chalk_1.default.gray(label)} ${displayBar} ${chalk_1.default.white(`$${item.cost.toFixed(2)}`)}`);
});
// しきい値の凡例表示
if (options.threshold) {
console.log('\n' + chalk_1.default.dim('Legend:'));
console.log(chalk_1.default.green('█') + ' < $20 (Less than Pro plan)');
if (scale >= proMax20) {
console.log(chalk_1.default.yellow('█') + ' $20-$200 (Exceeds Pro plan) ' + chalk_1.default.dim.yellow('|') + ' $20 threshold');
}
if (scale >= proMax200) {
console.log(chalk_1.default.red('█') + ' > $200 (Exceeds Pro Max plan) ' + chalk_1.default.dim.red('|') + ' $200 threshold');
}
console.log(chalk_1.default.dim(`Scale: $0 - $${scale}`));
}
}
//# sourceMappingURL=ccusage-graph.js.map