commit 153d08eeb44605bafa2ef2ca9b5614f50d90ca37 Author: ren Date: Mon Feb 16 19:29:28 2026 -0300 Initial commit: plan-usage - GLM & MiniMax statusline for Claude Code diff --git a/README.md b/README.md new file mode 100644 index 0000000..061a885 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# plan-usage + +简体中文 | [English](README_en.md) + +一个用于 Claude Code 的插件,在状态栏显示 GLM(智谱/ZAI)和 MiniMax 算力套餐的使用量统计。 + +![demo](screenshots/demo.png) + +## 功能特性 + +- 📊 **实时使用量追踪**: 同时显示 GLM 和 MiniMax 使用百分比 +- 🎨 **颜色警告提示**: 绿色 (0-70%)、黄色 (70-90%)、红色 (90-100%) +- ⚡ **快速查询**: 异步并行获取两个 API +- 🔧 **灵活配置**: 支持单独查看 GLM 或 MiniMax + +## 安装 + +### 方法 1: npm 安装(推荐) + +```bash +npm install -g @renato97/plan-usage +``` + +### 方法 2: 手动安装 + +```bash +mkdir -p ~/.claude/plan-usage +cp -r dist/index.js ~/.claude/plan-usage/ +chmod +x ~/.claude/plan-usage/index.js +``` + +## 配置 Claude Code + +编辑 `~/.claude/settings.json`: + +```json +{ + "statusLine": { + "type": "command", + "command": "plan-usage" + } +} +``` + +或者使用完整路径: + +```json +{ + "statusLine": { + "type": "command", + "command": "node ~/.claude/plan-usage/index.js" + } +} +``` + +## 环境变量 + +```bash +# GLM API Key (也会使用 ANTHROPIC_AUTH_TOKEN) +export GLM_API_KEY="your-glm-api-key" + +# MiniMax API Key (可选) +export MINIMAX_API_KEY="your-minimax-api-key" +``` + +## 使用方法 + +```bash +# 显示状态栏 (默认) +plan-usage + +# 只显示 GLM +plan-usage --glm + +# 只显示 MiniMax +plan-usage --minimax + +# JSON 格式输出 +plan-usage --json +``` + +## 输出示例 + +``` +GLM 6% | MiniMax 93% 1.6h +``` + +颜色说明: +- 🟢 绿色: 0-70% +- 🟡 黄色: 70-90% +- 🔴 红色: 90-100% + +## API 端点 + +- **GLM**: `https://api.z.ai/api/monitor/usage/quota/limit` +- **MiniMax**: `https://api.minimax.io/v1/api/openplatform/coding_plan/remains` + +## 构建 + +```bash +# 本地运行 +node dist/index.js + +# 测试 +node dist/index.js --json +``` + +## License + +MIT diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..a25f18e --- /dev/null +++ b/dist/index.js @@ -0,0 +1,212 @@ +#!/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' + } +}; + +// 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; + + for (const l of limits) { + if (l.quota_type === 'TOKENS_LIMIT') tokenPct = l.percentage || 0; + if (l.quota_type === 'TIME_LIMIT') timeRemaining = (l.remaining || 0) / 1000 / 60; + } + + return { tokenPct, timeRemaining, error: null }; + } catch (e) { + return { tokenPct: 0, timeRemaining: 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, 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 = (used / total) * 100; + + return { pct, remainingMinutes, model: m.model_name, error: null }; + } catch (e) { + return { pct: 0, remainingMinutes: 0, model: '?', 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}`; + } + + let status = `${colors.blue}GLM${colors.reset} ${getColor(glm.tokenPct)}${glm.tokenPct.toFixed(0)}%${colors.reset}`; + if (glm.timeRemaining > 0) { + status += ` ${formatTime(glm.timeRemaining)}`; + } + return status; +} + +// Format MiniMax status +function formatMinimaxStatus(minimax) { + if (minimax.error) { + return `${colors.purple}MiniMax${colors.reset} ${colors.red}offline${colors.reset}`; + } + + let status = `${colors.purple}MiniMax${colors.reset} ${getColor(minimax.pct)}${minimax.pct.toFixed(0)}%${colors.reset}`; + if (minimax.remainingMinutes > 0) { + status += ` ${formatTime(minimax.remainingMinutes)}`; + } + 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'); + + try { + const [glm, minimax] = await Promise.all([ + showOnlyMinimax ? Promise.resolve({ error: 'disabled' }) : getGlmUsage(), + showOnlyGlm ? Promise.resolve({ error: 'disabled' }) : getMinimaxUsage() + ]); + + if (showJson) { + console.log(JSON.stringify({ glm, minimax }, null, 2)); + return; + } + + // Statusline output + let parts = []; + + if (!showOnlyMinimax && !glm.error) { + parts.push(formatGlmStatus(glm)); + } + + if (!showOnlyGlm && !minimax.error) { + parts.push(formatMinimaxStatus(minimax)); + } + + 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(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..a5bd0f2 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "@renato97/plan-usage", + "version": "1.0.0", + "description": "GLM and MiniMax Coding Plan usage statusline for Claude Code", + "bin": { + "plan-usage": "./dist/index.js" + }, + "keywords": ["claude-code", "statusline", "glm", "minimax", "coding-plan"], + "author": "renato97", + "license": "MIT" +}