ccusage-on-cloud-client
Version:
Claude Code usage reporter client for ccusage-on-cloud-server
258 lines (223 loc) • 8.63 kB
JavaScript
const CLIENT_VERSION = '1.10.3';
const REQUIRED_SERVER_VERSION = '1.1.0';
// コマンドライン引数をチェック - ccusageコマンドを検出
const ccusageCommands = ['blocks', 'daily', 'monthly', 'session', 'mcp'];
const hasHelpFlag = process.argv.includes('--help') || process.argv.includes('-h');
const hasCcusageCommand = ccusageCommands.some(cmd => process.argv.includes(cmd));
const hasSendDataCommand = process.argv.includes('send-data');
if (hasCcusageCommand || hasHelpFlag) {
// wrapperコマンドに委譲
require('./ccusage-wrapper');
return;
}
if (hasSendDataCommand) {
// send-dataコマンドに委譲
require('./send-data');
return;
}
const cron = require('node-cron');
const { exec } = require('child_process');
const axios = require('axios');
const os = require('os');
const { validateConfig, printConfigHelp } = require('./lib/validator');
// 環境変数から設定を取得
const config = {
apiUrl: process.env.CCUSAGE_ON_CLOUD_API_URL || 'https://ccusage-on-cloud-server.onrender.com/usage',
username: process.env.CCUSAGE_ON_CLOUD_USERNAME,
password: process.env.CCUSAGE_ON_CLOUD_PASSWORD,
serverId: process.env.CCUSAGE_ON_CLOUD_SERVER_ID,
interval: process.env.CCUSAGE_ON_CLOUD_INTERVAL || '*/1 * * * *', // 1分ごと
displayMode: process.env.CCUSAGE_ON_CLOUD_DISPLAY_MODE === 'true', // データ表示モード
displayInterval: process.env.CCUSAGE_ON_CLOUD_DISPLAY_INTERVAL || '*/30 * * * * *', // 30秒ごと
};
// 設定の検証
const { errors, warnings } = validateConfig(config);
if (errors.length > 0) {
console.error('\n❌ Configuration errors detected:\n');
errors.forEach(error => console.error(error));
printConfigHelp();
process.exit(1);
}
if (warnings.length > 0) {
console.warn('\n⚠️ Configuration warnings:\n');
warnings.forEach(warning => console.warn(warning));
}
console.log(`🚀 ccusage-on-cloud-client started`);
console.log(`📡 API URL: ${config.apiUrl}`);
console.log(`👤 Username: ${config.username}`);
console.log(`🖥️ Server ID: ${config.serverId}`);
console.log(`⏰ Interval: ${config.interval}`);
if (config.displayMode) {
console.log(`📊 Display Mode: Enabled (${config.displayInterval})`);
}
// ccusageデータを取得する関数
async function getCcusageData() {
return new Promise((resolve, reject) => {
exec('npx ccusage@latest blocks -t max --json', (error, stdout, stderr) => {
if (error) {
reject(new Error(`ccusage command failed: ${error.message}`));
return;
}
try {
const data = JSON.parse(stdout);
// blocksデータにはccusageの詳細情報が含まれている
// 生のJSONLデータとしてblocksを送信
resolve({ blocks: data.blocks || [], rawJsonl: data.blocks || [] });
} catch (parseError) {
console.error('\n📄 Raw ccusage output:', stdout);
reject(new Error(`Failed to parse ccusage output: ${parseError.message}\n💡 Tip: Make sure ccusage is installed and working correctly`));
}
});
});
}
// APIにデータを送信する関数
async function sendUsageData(ccusageData) {
try {
// Basic認証用のヘッダーを作成
const auth = Buffer.from(`${config.username}:${config.password}`).toString('base64');
const payload = {
serverId: config.serverId,
blocks: ccusageData.blocks || [],
rawJsonl: ccusageData.rawJsonl || [],
timestamp: new Date().toISOString()
};
const response = await axios.post(config.apiUrl, payload, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${auth}`
},
timeout: 10000 // 10秒タイムアウト
});
console.log(`✅ Usage data sent successfully at ${new Date().toISOString()}`);
console.log(`📊 Blocks: ${payload.blocks.length}, Status: ${response.data.status}`);
// バージョンチェック
if (response.data.serverVersion) {
if (response.data.serverVersion !== REQUIRED_SERVER_VERSION) {
console.warn(`\n⚠️ Server version mismatch!`);
console.warn(` Client requires: v${REQUIRED_SERVER_VERSION}`);
console.warn(` Server running: v${response.data.serverVersion}`);
console.warn(` Some features may not work correctly.\n`);
}
} else {
console.warn(`\n⚠️ Server version unknown. Please update the server.\n`);
}
// 成功時のヒント
if (Math.random() < 0.1) { // 10%の確率でヒントを表示
console.log(`\n💡 Tip: View aggregated data with: npx ${process.env.npm_package_name || 'ccusage-on-cloud-client'} wrapper blocks\n`);
}
return response.data;
} catch (error) {
if (error.response) {
console.error(`❌ API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
if (error.response.status === 401) {
console.error('\n🔐 Authentication failed. Please check your username and password.');
} else if (error.response.status === 404) {
console.error('\n🔍 API endpoint not found. Please check CCUSAGE_ON_CLOUD_API_URL.');
}
} else if (error.request) {
console.error(`❌ Network Error: Could not reach ${config.apiUrl}`);
console.error('\n🌐 Please check:');
console.error(' - Is the server running?');
console.error(' - Is the URL correct?');
console.error(' - Are you connected to the internet?');
} else {
console.error(`❌ Error: ${error.message}`);
}
throw error;
}
}
// サーバーからデータを取得する関数
async function getUsageFromServer() {
try {
// Basic認証用のヘッダーを作成
const auth = Buffer.from(`${config.username}:${config.password}`).toString('base64');
const response = await axios.get(config.apiUrl, {
headers: {
'Authorization': `Basic ${auth}`
},
timeout: 10000
});
return response.data;
} catch (error) {
if (error.response) {
console.error(`❌ Failed to get data from server: ${error.response.status}`);
} else {
console.error(`❌ Failed to get data from server: ${error.message}`);
}
return null;
}
}
// サーバーのデータを表示する関数
function displayUsageData(data) {
if (!data || !data.usage || data.usage.length === 0) {
console.log(`📊 No usage data available for ${config.username}`);
return;
}
console.log(`\n📊 === Usage Report for ${data.username} ===`);
// 最新のデータから最大5件表示
const recentData = data.usage.slice(-5);
recentData.forEach((entry, index) => {
console.log(`\n📅 [${index + 1}] ${new Date(entry.timestamp).toLocaleString()}`);
console.log(` Server: ${entry.serverId}`);
if (entry.blocks && entry.blocks.length > 0) {
console.log(` Blocks:`);
entry.blocks.forEach(block => {
console.log(` - ${block.name}: ${block.count} ${block.unit}`);
});
}
});
console.log(`\n📊 Total records: ${data.usage.length}`);
console.log(`=====================================\n`);
}
// データを表示する処理
async function displayUsage() {
try {
console.log(`🔍 Fetching usage data from server...`);
const data = await getUsageFromServer();
if (data) {
displayUsageData(data);
}
} catch (error) {
console.error(`💥 Failed to display usage: ${error.message}`);
}
}
// メイン処理
async function reportUsage() {
try {
console.log(`🔄 Fetching ccusage data...`);
const ccusageData = await getCcusageData();
await sendUsageData(ccusageData);
} catch (error) {
console.error(`💥 Failed to report usage: ${error.message}`);
}
}
// 即座に1回実行
console.log(`🎯 Running initial usage report...`);
reportUsage();
// cronスケジュールで定期実行
console.log(`⏰ Scheduling usage reports with cron: ${config.interval}`);
cron.schedule(config.interval, () => {
reportUsage();
});
// 表示モードが有効な場合、定期的にデータを表示
if (config.displayMode) {
console.log(`📊 Scheduling usage display with cron: ${config.displayInterval}`);
// 初回表示(5秒後)
setTimeout(() => {
displayUsage();
}, 5000);
// 定期表示
cron.schedule(config.displayInterval, () => {
displayUsage();
});
}
// プロセス終了時の処理
process.on('SIGINT', () => {
console.log('\n🛑 ccusage-client stopped');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n🛑 ccusage-client terminated');
process.exit(0);
});