#!/usr/bin/env node /** * Plan Usage - GLM & MiniMax Coding Plan Statusline * Shows real-time usage for Claude Code * * Based on glm-plan-usage by jukanntenn * Modified to support both GLM and MiniMax * * Usage: * plan-usage # Show statusline * plan-usage --json # Show JSON output * plan-usage --glm # Show only GLM * plan-usage --minimax # Show only MiniMax */ const https = require('https'); // Colors const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', blue: '\x1b[36m', orange: '\x1b[38;5;208m', purple: '\x1b[35m' }; // API Configuration const CONFIG = { glm: { apiKey: process.env.GLM_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN, baseUrl: 'api.z.ai', endpoint: '/api/monitor/usage/quota/limit' }, minimax: { apiKey: process.env.MINIMAX_API_KEY || 'sk-cp-XC8cbgbVBuv1g8mMcao0ABeZu_rGEN_S22EhBUqo4lJbY_UJVqUVO5XF8hVobp8gE_39JbgQggr00TQwNdV9vP458Y_MBC_8GstvzmwhuukEGY4a2I5_L6A', baseUrl: 'api.minimax.io', endpoint: '/v1/api/openplatform/coding_plan/remains' }, kimi: { apiKey: process.env.KIMI_API_KEY || 'sk-kimi-dr5bfb3Gz8yOQOJVbLd2iY3kdDPf5MPBp1Ay467FlpAhvKMcTVOyiMH8zuQz1gb2', baseUrl: 'api.kimi.com', endpoint: '/coding/v1/usages' } }; // Make API request function makeRequest(options) { return new Promise((resolve, reject) => { const req = https.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }); req.on('error', reject); req.setTimeout(5000, () => { req.destroy(); reject(new Error('Timeout')); }); req.end(); }); } // Fetch GLM usage async function getGlmUsage() { if (!CONFIG.glm.apiKey) { return { error: 'No GLM API key configured' }; } try { const response = await makeRequest({ hostname: CONFIG.glm.baseUrl, path: CONFIG.glm.endpoint, method: 'GET', headers: { 'Authorization': `Bearer ${CONFIG.glm.apiKey}`, 'Content-Type': 'application/json' } }); const limits = response?.data?.limits || []; let tokenPct = 0; let timeRemaining = 0; let totalMinutes = 0; for (const l of limits) { if (l.type === 'TOKENS_LIMIT') { tokenPct = l.percentage || 0; if (l.nextResetTime) { const resetDate = new Date(l.nextResetTime); const now = new Date(); timeRemaining = (resetDate - now) / 1000 / 60; } } if (l.type === 'TIME_LIMIT') { if (l.nextResetTime && timeRemaining === 0) { const resetDate = new Date(l.nextResetTime); const now = new Date(); timeRemaining = (resetDate - now) / 1000 / 60; } } } return { tokenPct, timeRemaining, totalMinutes, error: null }; } catch (e) { return { tokenPct: 0, timeRemaining: 0, totalMinutes: 0, error: e.message }; } } // Fetch MiniMax usage async function getMinimaxUsage() { if (!CONFIG.minimax.apiKey) { return { error: 'No MiniMax API key configured' }; } try { const response = await makeRequest({ hostname: CONFIG.minimax.baseUrl, path: CONFIG.minimax.endpoint, method: 'GET', headers: { 'Authorization': `Bearer ${CONFIG.minimax.apiKey}`, 'Content-Type': 'application/json' } }); const m = response?.model_remains?.[0]; if (!m) { return { pct: 0, remainingMinutes: 0, totalMinutes: 0, model: '?', error: 'No data' }; } const remainingMinutes = m.remains_time / 1000 / 60; const used = m.current_interval_usage_count; const total = m.current_interval_total_count; const pct = 100 - ((used / total) * 100); const totalMinutes = remainingMinutes / (used / total) || 0; return { pct, remainingMinutes, totalMinutes, model: m.model_name, error: null }; } catch (e) { return { pct: 0, remainingMinutes: 0, totalMinutes: 0, model: '?', error: e.message }; } } // Fetch Kimi usage async function getKimiUsage() { if (!CONFIG.kimi.apiKey) { return { error: 'No Kimi API key configured' }; } try { const response = await makeRequest({ hostname: CONFIG.kimi.baseUrl, path: CONFIG.kimi.endpoint, method: 'GET', headers: { 'Authorization': `Bearer ${CONFIG.kimi.apiKey}`, 'Content-Type': 'application/json' } }); const usage = response?.usage; const limits = response?.limits || []; if (!usage) { return { pct: 0, remainingMinutes: 0, error: 'No data' }; } const pct = parseInt(usage.used) / parseInt(usage.limit) * 100; let remainingMinutes = 0; let weeklyReset = null; if (usage.resetTime) { const resetDate = new Date(usage.resetTime); const now = new Date(); remainingMinutes = (resetDate - now) / 1000 / 60; weeklyReset = resetDate; } const limitInfos = []; for (const l of limits) { if (l.detail?.resetTime) { const resetDate = new Date(l.detail.resetTime); const now = new Date(); const limitMins = (resetDate - now) / 1000 / 60; if (limitMins > 0) { const limitUsed = parseInt(l.detail.limit) - parseInt(l.detail.remaining); const limitPct = limitUsed / parseInt(l.detail.limit) * 100; const windowDur = l.window?.duration || 0; const windowUnit = l.window?.timeUnit || ''; let label = '5h'; if (windowUnit === 'TIME_UNIT_MINUTE' && windowDur >= 60) { label = `${windowDur / 60}h`; } limitInfos.push({ pct: limitPct, remainingMinutes: limitMins, label: label }); } } } return { pct, remainingMinutes, weeklyReset, limitInfos, error: null }; } catch (e) { return { pct: 0, remainingMinutes: 0, error: e.message }; } } // Get color based on percentage function getColor(pct) { if (pct > 90) return colors.red; if (pct > 70) return colors.yellow; return colors.green; } // Format time function formatTime(minutes) { if (minutes < 60) return `${minutes.toFixed(0)}m`; return `${(minutes/60).toFixed(1)}h`; } // Format GLM status function formatGlmStatus(glm) { if (glm.error) { return `${colors.blue}GLM${colors.reset} ${colors.red}offline${colors.reset}`; } const timeStr = glm.timeRemaining > 0 ? formatTime(glm.timeRemaining) : ''; return `${colors.blue}GLM${colors.reset} ${getColor(glm.tokenPct)}${glm.tokenPct.toFixed(0)}%${colors.reset}${timeStr ? ` ${timeStr}` : ''}`; } // Format MiniMax status function formatMinimaxStatus(minimax) { if (minimax.error) { return `${colors.purple}MiniMax${colors.reset} ${colors.red}offline${colors.reset}`; } const timeStr = minimax.remainingMinutes > 0 ? formatTime(minimax.remainingMinutes) : ''; return `${colors.purple}MiniMax${colors.reset} ${getColor(minimax.pct)}${minimax.pct.toFixed(0)}%${colors.reset}${timeStr ? ` ${timeStr}` : ''}`; } // Format Kimi status function formatKimiStatus(kimi) { if (kimi.error) { return `${colors.orange}Kimi${colors.reset} ${colors.red}offline${colors.reset}`; } let status = `${colors.orange}Kimi${colors.reset} ${getColor(kimi.pct)}${kimi.pct.toFixed(0)}%${colors.reset}`; if (kimi.remainingMinutes > 0) { status += ` ${formatTime(kimi.remainingMinutes)}`; } if (kimi.limitInfos && kimi.limitInfos.length > 0) { for (const lim of kimi.limitInfos) { status += ` ${colors.orange}${lim.label}${colors.reset}${getColor(lim.pct)}${lim.pct.toFixed(0)}%${colors.reset}`; } } return status; } // Main function async function main() { const args = process.argv.slice(2); const showJson = args.includes('--json'); const showOnlyGlm = args.includes('--glm'); const showOnlyMinimax = args.includes('--minimax'); const showOnlyKimi = args.includes('--kimi'); try { const [glm, minimax, kimi] = await Promise.all([ getGlmUsage(), getMinimaxUsage(), getKimiUsage() ]); if (showJson) { console.log(JSON.stringify({ glm, minimax, kimi }, null, 2)); return; } // Statusline output - always show all 3 let parts = []; if (!showOnlyMinimax && !showOnlyKimi) { parts.push(formatGlmStatus(glm)); } if (!showOnlyGlm && !showOnlyKimi) { parts.push(formatMinimaxStatus(minimax)); } if (!showOnlyGlm && !showOnlyMinimax) { parts.push(formatKimiStatus(kimi)); } if (parts.length === 0) { console.log(`${colors.red}No API configured${colors.reset}`); return; } console.log(parts.join(' | ')); } catch (e) { if (showJson) { console.log(JSON.stringify({ error: e.message }, null, 2)); } else { console.log(`${colors.red}Error: ${e.message}${colors.reset}`); } } } main();