UNPKG

ccusage-on-cloud-client

Version:

Claude Code usage reporter client for ccusage-on-cloud-server

258 lines (223 loc) 8.63 kB
#!/usr/bin/env node 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); });