322 lines
8.6 KiB
JavaScript
Executable File
322 lines
8.6 KiB
JavaScript
Executable File
#!/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,
|
|
baseUrl: 'api.minimax.io',
|
|
endpoint: '/v1/api/openplatform/coding_plan/remains'
|
|
},
|
|
kimi: {
|
|
apiKey: process.env.KIMI_API_KEY,
|
|
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();
|