diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 059d952..042af55 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -19,6 +19,12 @@ "source": "./plugins/glm-plan-bug", "category": "development", "description": "Submit case feedback and bug reports for GLM Coding Plan service." + }, + { + "name": "zai-quota-hud", + "source": "./plugins/zai-quota-hud", + "category": "monitoring", + "description": "Display ZAI/GLM Coding Plan quota usage in the Claude Code statusline with a color-coded progress bar, automatic color changes, and reset countdown." } ] } \ No newline at end of file diff --git a/README.md b/README.md index 6d8c8af..ffdad2a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A collection of plugins to enhance coding productivity and provide GLM Coding Pl |----------------------|-----------------------------------------------------------------------| | **glm-plan-usage** | Query quota and usage statistics for GLM Coding Plan | | **glm-plan-bug** | Submit case feedback and bug reports for GLM Coding Plan | +| **zai-quota-hud** | Display quota usage in the statusline with color-coded progress bar | **Attention:** **glm-plan-bug** will summarize your current conversation context to help identify issues. If you do not want to report this information, please do not actively use this plugin. @@ -42,6 +43,10 @@ claude plugin install glm-plan-usage@zai-coding-plugins claude plugin install glm-plan-bug@zai-coding-plugins ``` +```shell +claude plugin install zai-quota-hud@zai-coding-plugins +``` + ### Method B Run the `npx @z_ai/coding-helper` tool to manage and install the plugins directly. @@ -66,6 +71,14 @@ claude /glm-plan-bug:case-feedback Your feedback message here ``` +```bash +/zai-quota-hud:setup +``` + +```bash +/zai-quota-hud:quota +``` + **Attention:** **glm-plan-bug** will summarize your current conversation context to help identify issues. If you do not want to report this information, please do not actively use this plugin. ## Contributing diff --git a/README_CN.md b/README_CN.md index 8fef33f..77e42e4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -10,6 +10,7 @@ Claude Code 插件集合,旨在提升编程效率和提供 GLM Coding Plan 相 |---------------------|------------------------------------| | **glm-plan-usage** | 查询 GLM Coding Plan 的配额和使用统计 | | **glm-plan-bug** | 提交 GLM Coding Plan 的反馈和问题报告 | +| **zai-quota-hud** | 在状态栏显示配额使用量,带颜色编码进度条 | **注意:** **glm-plan-bug** 会总结您当前对话的上下文信息以帮助定位问题,若不想上报信息,请勿主动使用此插件。 @@ -42,6 +43,10 @@ claude plugin install glm-plan-usage@zai-coding-plugins claude plugin install glm-plan-bug@zai-coding-plugins ``` +```shell +claude plugin install zai-quota-hud@zai-coding-plugins +``` + ### 方式二 运行 `npx @z_ai/coding-helper` 工具直接管理和安装插件。 @@ -66,6 +71,14 @@ claude /glm-plan-bug:case-feedback 在此输入你的反馈 ``` +```bash +/zai-quota-hud:setup +``` + +```bash +/zai-quota-hud:quota +``` + **注意:** **glm-plan-bug** 会总结您当前对话的上下文信息以帮助定位问题,若不想上报信息,请勿主动使用此插件。 ## 贡献 diff --git a/plugins/zai-quota-hud/.claude-plugin/plugin.json b/plugins/zai-quota-hud/.claude-plugin/plugin.json new file mode 100644 index 0000000..278bda9 --- /dev/null +++ b/plugins/zai-quota-hud/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "zai-quota-hud", + "description": "Display ZAI/GLM Coding Plan quota usage in the Claude Code statusline with a color-coded progress bar", + "version": "1.0.0", + "author": { + "name": "n1majne3", + "url": "https://github.com/n1majne3" + }, + "commands": [ + "./commands/setup.md", + "./commands/quota.md" + ], + "license": "MIT", + "keywords": ["zai", "zhipu", "glm", "quota", "statusline", "claude-code"] +} diff --git a/plugins/zai-quota-hud/README.md b/plugins/zai-quota-hud/README.md new file mode 100644 index 0000000..d3f3ddc --- /dev/null +++ b/plugins/zai-quota-hud/README.md @@ -0,0 +1,40 @@ +# ZAI Quota HUD + +[中文](README.zh-CN.md) + +A Claude Code plugin that displays your ZAI/GLM Coding Plan token quota usage in the statusline. + +![ZAI ██░░░ 37%](https://img.shields.io/badge/statusline-quota-brightgreen) ![License](https://img.shields.io/github/license/n1majne3/zai-quota-hud) + +## What you see + +Below the input bar: + +``` +ZAI ██░░░ 37% +``` + +Color changes automatically: green (< 50%), yellow (50-80%), red (> 80%). + +At 100% usage, a countdown timer shows time until reset: + +``` +ZAI █████ 100% ⏳ 1h 23m +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `/zai-quota-hud:setup` | Configure the statusline in Claude Code settings | +| `/zai-quota-hud:quota` | Show detailed quota breakdown (per-model) | + +## Requirements + +- Claude Code v1.0.80+ +- Node.js 18+ +- `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` environment variables set (ZAI or Zhipu endpoint) + +## License + +MIT diff --git a/plugins/zai-quota-hud/commands/quota.md b/plugins/zai-quota-hud/commands/quota.md new file mode 100644 index 0000000..b78455c --- /dev/null +++ b/plugins/zai-quota-hud/commands/quota.md @@ -0,0 +1,89 @@ +--- +description: Query detailed ZAI/GLM Coding Plan quota and usage breakdown +allowed-tools: Bash, Read +--- + +# Query ZAI Quota Details + +Run a detailed quota query and show the full breakdown to the user. + +## Execution + +### Step 1: Run the query script + +Execute the statusline script to get fresh data (bust cache by removing the cache file first): + +```bash +rm -f /tmp/zai-quota-cache.json && echo '{}' | node "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache/zai-quota-hud/zai-quota-hud/$(ls "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/zai-quota-hud/zai-quota-hud/ | sort | tail -1)/scripts/statusline.mjs" +``` + +If the plugin is loaded via `--plugin-dir`, adjust the path accordingly. You can find the plugin root by looking for `scripts/statusline.mjs` relative to the plugin directory. + +If that fails, try: +```bash +rm -f /tmp/zai-quota-cache.json && ANTHROPIC_BASE_URL="$ANTHROPIC_BASE_URL" ANTHROPIC_AUTH_TOKEN="$ANTHROPIC_AUTH_TOKEN" node -e " +const https = require('https'); +const { URL } = require('url'); +const baseUrl = process.env.ANTHROPIC_BASE_URL || ''; +const authToken = process.env.ANTHROPIC_AUTH_TOKEN || ''; +if (!baseUrl || !authToken) { console.error('Set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN'); process.exit(1); } +const parsed = new URL(baseUrl); +const base = parsed.protocol + '//' + parsed.host; +const url = new URL(base + '/api/monitor/usage/quota/limit'); +const req = https.request({ hostname: url.hostname, port: url.port || 443, path: url.pathname, method: 'GET', headers: { 'Authorization': authToken, 'Accept-Language': 'en-US,en', 'Content-Type': 'application/json' } }, (res) => { + let d = ''; res.on('data', c => d += c); res.on('end', () => { console.log(JSON.stringify(JSON.parse(d), null, 2)); }); +}); +req.on('error', e => { console.error(e.message); process.exit(1); }); +req.end(); +" +``` + +### Step 2: Format the output + +Parse the JSON response and present it clearly: + +**If platform is ZHIPU** (ANTHROPIC_BASE_URL contains `open.bigmodel.cn`), present in Chinese: + +``` +## ZAI 配额使用情况 + +**账号等级:** {level} + +### Token 使用 (5小时窗口) +- 使用率: {percentage}% +- 下次重置: {nextResetTime formatted} + +### MCP 使用 (月度) +- 当前用量: {currentValue} / {usage} +- 剩余: {remaining} +- 使用率: {percentage}% +- 下次重置: {nextResetTime formatted} + +#### 模型明细: +- {modelCode}: {usage} +``` + +**If platform is ZAI** (ANTHROPIC_BASE_URL contains `api.z.ai`), present in English: + +``` +## ZAI Quota Usage + +**Account Level:** {level} + +### Token Usage (5-hour window) +- Usage: {percentage}% +- Resets at: {nextResetTime formatted} + +### MCP Usage (Monthly) +- Current: {currentValue} / {usage} +- Remaining: {remaining} +- Usage: {percentage}% +- Resets at: {nextResetTime formatted} + +#### Per-model breakdown: +- {modelCode}: {usage} +``` + +### Constraint + +**Run the query exactly once.** Do not retry on failure. Show the error if it fails. diff --git a/plugins/zai-quota-hud/commands/setup.md b/plugins/zai-quota-hud/commands/setup.md new file mode 100644 index 0000000..f9a2257 --- /dev/null +++ b/plugins/zai-quota-hud/commands/setup.md @@ -0,0 +1,68 @@ +--- +description: Configure the ZAI Quota HUD statusline in Claude Code settings +allowed-tools: Bash, Read, Write, Edit +--- + +# Setup ZAI Quota HUD Statusline + +Configure the ZAI Quota HUD to display quota information below the Claude Code input bar. + +## Steps + +### 1. Detect the plugin directory + +Find where the zai-quota-hud plugin is installed. Check in order: +1. `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache/zai-quota-hud/` (marketplace install) +2. Look for a `--plugin-dir` loaded copy +3. Ask the user for the plugin path + +Run: `ls -d "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/zai-quota-hud/zai-quota-hud/*/ 2>/dev/null | sort | tail -1` + +If that returns a path, use it. If not, ask the user for the absolute path to the `zai-usage-claude-code-plugin` directory. + +### 2. Generate the statusline command + +For a marketplace-installed plugin: +``` +bash -c 'plugin_dir=$(ls -d "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/zai-quota-hud/zai-quota-hud/*/ 2>/dev/null | sort | tail -1); exec node "${plugin_dir}scripts/statusline.mjs"' +``` + +For a local plugin-dir install: +``` +bash -c 'exec node "/scripts/statusline.mjs"' +``` + +Where `` is the absolute path from step 1. + +### 3. Write the statusLine config + +Read the current `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json` (create it if it doesn't exist). + +Merge in the `statusLine` field while preserving all existing settings: + +```json +{ + "statusLine": { + "type": "command", + "command": "" + } +} +``` + +If a `statusLine` already exists, confirm with the user before overwriting. + +### 4. Verify + +Run the generated command to confirm it produces output: + +```bash +echo '{}' | bash -c '' +``` + +### 5. Inform the user + +Tell the user: +- The statusline has been configured +- They need to **restart Claude Code** for the statusLine config to take effect +- After restart, they should see quota info below the input bar +- If they don't see it, run `/zai-quota-hud:setup` again or check that `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` are set in their environment diff --git a/plugins/zai-quota-hud/scripts/statusline.mjs b/plugins/zai-quota-hud/scripts/statusline.mjs new file mode 100755 index 0000000..ea4d230 --- /dev/null +++ b/plugins/zai-quota-hud/scripts/statusline.mjs @@ -0,0 +1,186 @@ +#!/usr/bin/env node + +import https from 'https'; +import fs from 'fs'; +import { URL } from 'url'; + +const CACHE_PATH = '/tmp/zai-quota-cache.json'; +const CACHE_TTL_MS = 60_000; + +const RESET = '\x1b[0m'; +const DIM = '\x1b[2m'; +const GREEN = '\x1b[32m'; +const YELLOW = '\x1b[33m'; +const RED = '\x1b[31m'; + +function colorForPercent(pct) { + if (pct >= 80) return RED; + if (pct >= 50) return YELLOW; + return GREEN; +} + +function bar(pct, width = 10) { + const filled = Math.round((pct / 100) * width); + const empty = width - filled; + return '█'.repeat(filled) + '░'.repeat(empty); +} + +function formatCountdown(nextResetTime) { + const ms = new Date(nextResetTime).getTime() - Date.now(); + if (ms <= 0) return null; + const totalMin = Math.floor(ms / 60_000); + const h = Math.floor(totalMin / 60); + const m = totalMin % 60; + const s = Math.floor((ms % 60_000) / 1000); + if (h > 0) return `${h}h ${m}m`; + if (m > 0) return `${m}m`; + return `${s}s`; +} + +function readCache() { + try { + const stat = fs.statSync(CACHE_PATH); + if (Date.now() - stat.mtimeMs < CACHE_TTL_MS) { + const raw = fs.readFileSync(CACHE_PATH, 'utf8'); + return JSON.parse(raw); + } + } catch {} + return null; +} + +function writeCache(data) { + try { + fs.writeFileSync(CACHE_PATH, JSON.stringify(data)); + } catch {} +} + +function fetchQuota(baseUrl, authToken) { + return new Promise((resolve, reject) => { + let parsed; + try { + parsed = new URL(baseUrl); + } catch { + return reject(new Error('Invalid ANTHROPIC_BASE_URL')); + } + + const baseDomain = `${parsed.protocol}//${parsed.host}`; + const apiUrl = `${baseDomain}/api/monitor/usage/quota/limit`; + const url = new URL(apiUrl); + + const options = { + hostname: url.hostname, + port: url.port || 443, + path: url.pathname, + method: 'GET', + headers: { + 'Authorization': authToken, + 'Accept-Language': 'en-US,en', + 'Content-Type': 'application/json', + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + if (res.statusCode !== 200) { + return reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + try { + const json = JSON.parse(data); + if (json.success && json.data) { + resolve(json.data); + } else { + reject(new Error(json.msg || 'API returned unsuccessful response')); + } + } catch (e) { + reject(new Error(`Parse error: ${e.message}`)); + } + }); + }); + + req.on('error', reject); + req.setTimeout(5000, () => { + req.destroy(); + reject(new Error('Request timeout')); + }); + req.end(); + }); +} + +function formatOutput(data) { + const tokensLimit = (data.limits || []).find((l) => l.type === 'TOKENS_LIMIT'); + if (!tokensLimit) return `${DIM}ZAI: no data${RESET}`; + const pct = tokensLimit.percentage || 0; + let suffix = ''; + if (pct >= 100 && tokensLimit.nextResetTime) { + const cd = formatCountdown(tokensLimit.nextResetTime); + if (cd) suffix = ` ⏳ ${cd}`; + } + return `${colorForPercent(pct)}ZAI ${bar(pct, 5)} ${pct}%${suffix}${RESET}`; +} + +function readStdin() { + return new Promise((resolve) => { + if (process.stdin.isTTY) { + resolve(null); + return; + } + let data = ''; + process.stdin.setEncoding('utf8'); + const timer = setTimeout(() => { + resolve(null); + }, 250); + process.stdin.on('data', (chunk) => { data += chunk; }); + process.stdin.on('end', () => { + clearTimeout(timer); + try { + resolve(JSON.parse(data.trim())); + } catch { + resolve(null); + } + }); + }); +} + +async function main() { + const baseUrl = process.env.ANTHROPIC_BASE_URL || ''; + const authToken = process.env.ANTHROPIC_AUTH_TOKEN || ''; + + if (!authToken || !baseUrl) { + console.log(`${DIM}ZAI: not configured (set ANTHROPIC_BASE_URL & ANTHROPIC_AUTH_TOKEN)${RESET}`); + return; + } + + let isZai = baseUrl.includes('api.z.ai'); + let isZhipu = baseUrl.includes('open.bigmodel.cn') || baseUrl.includes('dev.bigmodel.cn'); + if (!isZai && !isZhipu) { + console.log(`${DIM}ZAI: unsupported base URL${RESET}`); + return; + } + + await readStdin(); + + let data = readCache(); + + if (!data) { + try { + data = await fetchQuota(baseUrl, authToken); + writeCache(data); + } catch (e) { + const stale = readCache(); + if (stale) { + console.log(formatOutput(stale) + ` ${DIM}(stale)${RESET}`); + } else { + console.log(`${DIM}ZAI: unavailable (${e.message})${RESET}`); + } + return; + } + } + + console.log(formatOutput(data)); +} + +main().catch((e) => { + console.log(`${DIM}ZAI: error (${e.message})${RESET}`); +});