From 0caa5732d31c4ffc2c2d69203acc3fdd7bbf4472 Mon Sep 17 00:00:00 2001 From: Jovan <3071058281@qq.com> Date: Tue, 22 Sep 2026 00:42:43 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A6=20NEW:=20Command=20Code=20Usage=20?= =?UTF-8?q?-=20check=20plan=20quota=20windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + skills/command-code-usage/SKILL.md | 147 +++++++++ .../command-code-usage/scripts/cc-usage.mjs | 303 ++++++++++++++++++ 3 files changed, 451 insertions(+) create mode 100644 skills/command-code-usage/SKILL.md create mode 100644 skills/command-code-usage/scripts/cc-usage.mjs diff --git a/README.md b/README.md index 18ab938..0b11553 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Skills work seamlessly with **Command Code** providing consistent, high-quality - **[artifacts-builder](skills/artifacts-builder/)** - Suite of tools for creating elaborate, multi-component HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). - **[Brainstorming](skills/brainstorming/)** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation, and saves design document. - **[Changelog Generator](skills/changelog-generator/)** - Automatically creates user-facing changelogs from git commits by analyzing history and transforming technical commits into customer-friendly release notes. +- **[Command Code Usage](skills/command-code-usage/)** - Checks how much of a Command Code plan is left - the 5-hour, weekly and monthly quota windows, with reset times - so a task can be sized against the quota before it runs out. - **[MCP Builder](skills/mcp-builder/)** - Guides creation of high-quality MCP (Model Context Protocol) servers for integrating external APIs and services with LLMs using Python or TypeScript. - **[Playwright](skills/playwright-skill/)** - Complete browser automation with Playwright. Auto-detects dev servers, writes clean test scripts, tests pages, fills forms, takes screenshots, checks responsive design, validates UX, tests login flows, and automates any browser task. - **[Receiving Code Review](skills/receiving-code-review/)** - Handles code review feedback with technical rigor and verification. Requires understanding before implementing, asks clarifying questions, and evaluates suggestions against codebase reality. diff --git a/skills/command-code-usage/SKILL.md b/skills/command-code-usage/SKILL.md new file mode 100644 index 0000000..5fadf83 --- /dev/null +++ b/skills/command-code-usage/SKILL.md @@ -0,0 +1,147 @@ +--- +name: command-code-usage +description: Check how much of a Command Code plan is left. Reads the 5-hour, weekly and monthly quota windows - remaining amount, percentage and reset time - from the Command Code API, using the credential already on the machine. Use it when the user asks how much quota is left, whether the plan can finish a task, or when they hit a limit. +--- + +# Command Code Usage + +Command Code reports quota per rolling window, but only on the web dashboard and in the +CLI's own screens. This skill answers the questions a user actually asks mid-work - *how +much is left, and does it last until the reset* - without them leaving the editor. + +Everything runs locally: one script, no dependencies, no telemetry. Checking the quota +does not consume any. + +## When to Use This Skill + +- "How much of my Command Code plan is left?" +- "Can I finish this refactor on the current 5-hour window?" +- "When does my usage reset?" +- "Did that big run eat the month?" / "Am I going to run out before Friday?" +- The user just hit a rate limit or quota error and wants to know how long it lasts. + +Do not use it to change anything about the plan - it only reads. + +## Prerequisites + +- **Node 18+** (the script uses the built-in `fetch`). +- **A Command Code credential.** In order of lookup: + 1. `COMMAND_CODE_API_KEY`, `COMMANDCODE_API_KEY` or `CMD_API_KEY` in the environment. + 2. `~/.commandcode/auth.json`, written when the user signs in with the Command Code CLI. + + If neither exists, the script exits with code 2 and says so - then ask the user to sign + in with the Command Code CLI rather than asking them to paste a key into the chat. If + their key lives inside a host's provider config instead (for example + `~/.zcode/v2/provider_config.json`), it can be passed as `COMMAND_CODE_API_KEY` for one + call; never echo the value back. + +## How to Use + +Run the script and show its output as it is printed: + +```bash +node scripts/cc-usage.mjs +``` + +``` +GOAT / alex + 5h ▌░░░░░░░░░ 5% $13.32 left of $14.00 resets 09-22 04:16 (in 3h35m) + week ███▊░░░░░░ 37% $22.00 left of $35.00 resets 09-27 23:29 (in 5d22h) + month █▉░░░░░░░░ 19% $57.00 left of $70.00 resets 10-20 23:25 (in 28d22h) + period 2891 requests / $10.22 spent / avg $0.0035 each + the month has room for about 16120 more at that price +``` + +Do not recompute the numbers or redraw the bars - the script already normalized the API's +raw fields, and a second pass is how the answer drifts. To answer a follow-up, quote the +row it came from. + +For anything programmatic - a badge, a summary line, a comparison over time - use the JSON: + +```bash +node scripts/cc-usage.mjs --json +``` + +```json +{ + "plan": "GOAT", + "status": "active", + "limited": true, + "windows": { + "fiveHour": { "used": 0.67, "cap": 14, "percent": 4.8, "remaining": 13.32, "resetAt": 1790021805875, "resetsInMs": 12937376, "exceeded": false }, + "weekly": { "used": 12.99, "cap": 35, "percent": 37.1, "remaining": 22.0, "resetAt": 1790436566000, "resetsInMs": 503376000 } + }, + "monthly": { "used": 12.99, "cap": 70, "remaining": 57.0, "percent": 18.6, "resetAt": 1792548325000, "resetsInMs": 2490000000 }, + "spend": { "total": 10.22, "perRequest": 0.0035, "requests": 2891, "failed": 0, "successRate": 100 } +} +``` + +## Reading the Numbers + +The unit is **US dollars of plan value**, not requests. A window's `cap` is what the plan +allows and `used` is what has been spent against it; one expensive model can consume a +window that a cheap one barely touches. Never translate to "messages" unless the script +already did, and then say the price it used. + +- `percent` is clamped to 100. If a window reports `exceeded: true`, say so - the plan is + over its limit and requests may be refused or billed differently. +- `resetAt` and `currentPeriodEnd` are epoch milliseconds. Always convert to the user's + local time; a raw timestamp is not an answer. +- `resetsInMs` is the countdown, and it is what "will it last?" hinges on. A window at 40% + with four hours left is comfortable; the same 40% with twenty minutes left is not. +- `limited: false` means the account has no rolling windows at all. `windows.fiveHour` and + `windows.weekly` are then `null`, and the monthly pool is the whole story. Do not report + that as missing data. +- `monthly.cap` is the plan's face value while the subscription is active, otherwise what + has been spent plus what is left. `spend.*` describes the current billing period and is a + different question from the quota windows - do not add the two together. +- `spend.perRequest` is the average price of a request **in this period** (around + `$0.0035`). It is an average over whatever model mix the user has been running, so any + request count derived from it is an estimate - say "about", and name the price it came + from. Switching to a more expensive model makes the real number smaller. + +## Going Beyond the Script + +The script covers the usual questions. If you need a field it does not expose, these are +the endpoints behind it (all `GET`, all with `Authorization: Bearer `): + +| Endpoint | What it returns | +|---|---| +| `/alpha/whoami?limits=1` | the account, the org, and the org's limits | +| `/alpha/billing/credits?orgId=` | `credits` (`monthlyCredits`, `purchasedCredits`, `freeCredits`) and `windowLimits` (`limited`, `fiveHour`, `weekly`, each `{used, cap, resetAt, exceeded}`) | +| `/alpha/billing/subscriptions?orgId=` | `data` with `planId`, `status`, `currentPeriodStart`, `currentPeriodEnd` | +| `/alpha/usage/summary?since=` | `totalCount`, `totalCost`, `averageCost`, `successRate`, token totals for the period | + +Base URL `https://api.commandcode.ai`, overridable with `COMMAND_CODE_API_BASE`. For a team +account, pass `?orgId=` to the billing and usage calls; `/alpha/whoami` gives the id. + +## Plans + +The window sizes per plan, used only when the API does not report a `cap`: + +| Plan (`planId`) | Month | 5 hours | Week | +|---|---|---|---| +| `individual-go` | $10 | $3 | $6 | +| `individual-goat` | $70 | $14 | $35 | +| `individual-pro` | $80 | $16 | $40 | +| `individual-max-10x` | $150 | $45 | $90 | +| `individual-max-20x` | $300 | $90 | $180 | +| `teams-pro` | $40 | $12 | $24 | + +An account on something not listed here still works: the API's own `cap` values win, and +the table is only a fallback. If a user's plan reports no `cap` and no table entry, report +what was actually used instead of guessing a denominator. + +## Troubleshooting + +| What you see | What it means | +|---|---| +| exit code 2, "No Command Code credential found" | Neither the environment variable nor `~/.commandcode/auth.json` exists. Ask the user to sign in with the Command Code CLI. | +| exit code 1, `HTTP 401` / `HTTP 403` | The credential was rejected - it is stale or from a different account. Signing in again with the CLI rewrites `auth.json`. | +| exit code 1, `HTTP 429` | The API rate-limited this machine. Wait a minute, then retry once - a run makes four calls. | +| `5h not applicable on this plan` | `windowLimits.limited` is `false`. Expected, not a failure. | +| `note: /alpha/... -> HTTP 500` | One endpoint failed and the rest still answered. Report the rows you have and mention the failing endpoint; do not retry more than once. | +| All four endpoints fail | The script exits 1 with the list. Usually a network problem or a wrong `COMMAND_CODE_API_BASE`. | + +The script only reads. It writes no files, sends nothing anywhere except the API, and exits +non-zero rather than printing a partial answer it cannot vouch for. diff --git a/skills/command-code-usage/scripts/cc-usage.mjs b/skills/command-code-usage/scripts/cc-usage.mjs new file mode 100644 index 0000000..208f60e --- /dev/null +++ b/skills/command-code-usage/scripts/cc-usage.mjs @@ -0,0 +1,303 @@ +#!/usr/bin/env node +/** + * Command Code usage - how much of the plan is left. + * + * Reads the three quota windows Command Code reports (5-hour, weekly, monthly) + * and prints them. Zero dependencies, Node 18+, no network call except to the + * Command Code API, no writes anywhere. + * + * node cc-usage.mjs # the panel + * node cc-usage.mjs --json # the same numbers, machine-readable + * + * Credential: COMMAND_CODE_API_KEY (or COMMANDCODE_API_KEY / CMD_API_KEY), else + * the login the Command Code CLI already stored at ~/.commandcode/auth.json. + */ + +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +const API_BASE = (process.env.COMMAND_CODE_API_BASE || 'https://api.commandcode.ai').replace(/\/+$/, ''); +const TIMEOUT_MS = 15000; + +// The API reports what a window has *used*; the size of the window is a property +// of the plan. `windowLimits..cap` normally carries it, and this table is +// only the fallback for when it does not - so keep it in step with the plans. +const PLANS = { + 'individual-go': { name: 'Go', monthly: 10, fiveHour: 3, weekly: 6 }, + 'individual-goat': { name: 'GOAT', monthly: 70, fiveHour: 14, weekly: 35 }, + 'individual-pro': { name: 'Pro', monthly: 80, fiveHour: 16, weekly: 40 }, + 'individual-max-10x': { name: 'Max 10x', monthly: 150, fiveHour: 45, weekly: 90 }, + 'individual-max-20x': { name: 'Max 20x', monthly: 300, fiveHour: 90, weekly: 180 }, + 'teams-pro': { name: 'Team Pro', monthly: 40, fiveHour: 12, weekly: 24 }, +}; + +/* --------------------------------------------------------------- credentials */ + +function readJson(file) { + try { + return JSON.parse(fs.readFileSync(file, 'utf8')); + } catch { + return null; + } +} + +export function resolveCredential(env = process.env) { + for (const name of ['COMMAND_CODE_API_KEY', 'COMMANDCODE_API_KEY', 'CMD_API_KEY']) { + const value = env[name]; + if (value && value.trim()) return { apiKey: value.trim(), source: `$${name}` }; + } + const file = path.join(os.homedir(), '.commandcode', 'auth.json'); + const doc = readJson(file); + if (doc && typeof doc.apiKey === 'string' && doc.apiKey.trim()) { + return { apiKey: doc.apiKey.trim(), source: file }; + } + return null; +} + +/* ------------------------------------------------------------------ the API */ + +async function apiGet(route, apiKey, query = {}) { + const url = new URL(API_BASE + route); + for (const [key, value] of Object.entries(query)) { + if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, String(value)); + } + const abort = new AbortController(); + const timer = setTimeout(() => abort.abort(), TIMEOUT_MS); + try { + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, + signal: abort.signal, + }); + const text = await res.text(); + let body = null; + try { + body = JSON.parse(text); + } catch { + body = null; + } + if (!res.ok) { + const detail = body?.error?.message || body?.message || text.slice(0, 200) || res.statusText; + throw new Error(`${route} -> HTTP ${res.status}: ${detail}`); + } + if (!body || typeof body !== 'object') throw new Error(`${route} -> response was not JSON`); + return body; + } finally { + clearTimeout(timer); + } +} + +// A window that fails is not a reason to lose the whole panel; the caller decides +// what to do with the report. Returns { data } or { error }. +async function soft(promise) { + try { + return { data: await promise }; + } catch (error) { + return { error: error.message }; + } +} + +export async function collectUsage(apiKey, { orgId } = {}) { + const whoami = await soft(apiGet('/alpha/whoami', apiKey, { limits: '1' })); + const org = orgId || whoami.data?.org?.id || undefined; + const scope = org ? { orgId: org } : {}; + + const [credits, subscriptions] = await Promise.all([ + soft(apiGet('/alpha/billing/credits', apiKey, scope)), + soft(apiGet('/alpha/billing/subscriptions', apiKey, scope)), + ]); + + const since = subscriptions.data?.data?.currentPeriodStart; + const summary = await soft(apiGet('/alpha/usage/summary', apiKey, { ...scope, since })); + + const errors = [whoami.error, credits.error, subscriptions.error, summary.error].filter(Boolean); + if (errors.length === 4) throw new Error(`could not read usage:\n - ${errors.join('\n - ')}`); + return { whoami: whoami.data, credits: credits.data, subscription: subscriptions.data, summary: summary.data, errors }; +} + +/* -------------------------------------------------------------- normalizing */ + +const asMoney = (value) => Math.max(0, Number(value) || 0); + +export function normalize(raw, now = Date.now()) { + const credits = raw.credits?.credits ?? {}; + const limits = raw.credits?.windowLimits ?? null; + const subscription = raw.subscription?.data ?? null; + const plan = PLANS[subscription?.planId] ?? null; + const periodEnd = subscription?.currentPeriodEnd ? Date.parse(subscription.currentPeriodEnd) : null; + + // Two ways to answer "how big is the month": the plan's face value while the + // subscription is active, otherwise what has been spent plus what is left. + const monthlyLeft = asMoney(credits.monthlyCredits); + const purchased = asMoney(credits.purchasedCredits); + const free = asMoney(credits.freeCredits); + const remaining = monthlyLeft + purchased + free; + const planMonthly = subscription?.status === 'active' && plan ? plan.monthly : null; + const pool = planMonthly !== null ? Math.max(planMonthly, monthlyLeft) + purchased + free : asMoney(raw.summary?.totalCost) + remaining; + const spent = Math.max(0, pool - remaining); + + const window = (spec, fallbackCap) => { + if (!spec) return null; + const used = asMoney(spec.used); + const cap = Number(spec.cap) || fallbackCap || 0; + const resetAt = Number(spec.resetAt) || null; + return { + used, + cap, + percent: cap > 0 ? Math.min((used / cap) * 100, 100) : 0, + remaining: Math.max(0, cap - used), + resetAt, + resetsInMs: resetAt ? Math.max(0, resetAt - now) : null, + exceeded: Boolean(spec.exceeded), + }; + }; + + return { + plan: plan ? plan.name : null, + planId: subscription?.planId ?? null, + status: subscription?.status ?? null, + user: raw.whoami?.user?.userName ?? raw.whoami?.user?.name ?? null, + org: raw.whoami?.org?.name ?? null, + // `limited: false` means this account has no rolling windows at all - the + // monthly pool is the whole story, and the two nulls below are the truth. + limited: limits ? Boolean(limits.limited) : null, + windows: + limits?.limited === false + ? { fiveHour: null, weekly: null } + : { fiveHour: window(limits?.fiveHour, plan?.fiveHour), weekly: window(limits?.weekly, plan?.weekly) }, + monthly: { + used: spent, + cap: pool, + remaining, + percent: pool > 0 ? Math.min((spent / pool) * 100, 100) : 0, + resetAt: periodEnd, + resetsInMs: periodEnd ? Math.max(0, periodEnd - now) : null, + }, + spend: { + total: asMoney(raw.summary?.totalCost), + perRequest: Number(raw.summary?.averageCost) > 0 ? Number(raw.summary.averageCost) : null, + requests: Number(raw.summary?.totalCount) || null, + failed: Number(raw.summary?.failedCount) || 0, + successRate: Number.isFinite(Number(raw.summary?.successRate)) ? Number(raw.summary.successRate) : null, + }, + errors: raw.errors ?? [], + }; +} + +/* ---------------------------------------------------------------- rendering */ + +const ESC = String.fromCharCode(27); +const BLOCK = String.fromCharCode(9608); // full block +const SHADE = String.fromCharCode(9617); // light shade +// Eighth-blocks, low to high, so a bar can stop between two cells. +const EIGHTHS = [0x258f, 0x258e, 0x258d, 0x258c, 0x258b, 0x258a, 0x2589].map((c) => String.fromCharCode(c)); +const AMBER = 70; +const RED = 90; + +function paint(percent, text) { + if (process.env.NO_COLOR || !process.stdout.isTTY) return text; + const code = percent >= RED ? 31 : percent >= AMBER ? 33 : 32; + return `${ESC}[${code}m${text}${ESC}[0m`; +} + +function bar(percent, width = 10) { + const exact = (Math.max(0, Math.min(percent, 100)) / 100) * width; + const full = Math.floor(exact); + const partial = full < width ? EIGHTHS[Math.min(6, Math.round((exact - full) * 8) - 1)] || ' ' : ''; + return BLOCK.repeat(full) + partial + SHADE.repeat(Math.max(0, width - full - 1)); +} + +const money = (n) => `$${n.toFixed(2)}`; +// A per-request price is almost always under a cent, where two decimals read as $0.00. +const price = (n) => `$${n.toFixed(n >= 0.01 ? 2 : 4)}`; + +function duration(ms) { + if (!ms) return ''; + const minutes = Math.floor(ms / 60000); + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h${String(minutes % 60).padStart(2, '0')}m`; + return `${Math.floor(hours / 24)}d${String(hours % 24).padStart(2, '0')}h`; +} + +function clock(ms) { + if (!ms) return ''; + const d = new Date(ms); + const pad = (n) => String(n).padStart(2, '0'); + return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +function render(view) { + const lines = []; + const who = [view.plan || view.planId || 'Command Code', view.user].filter(Boolean).join(' / '); + lines.push(who + (view.status && view.status !== 'active' ? ` (${view.status})` : '')); + + const row = (label, window, missing) => { + if (!window) { + lines.push(` ${label.padEnd(5)} ${missing}`); + return; + } + const amount = window.cap > 0 ? `${money(window.remaining)} left of ${money(window.cap)}` : `${money(window.used)} used`; + const reset = window.resetAt ? `resets ${clock(window.resetAt)} (in ${duration(window.resetsInMs)})` : 'no reset scheduled'; + const pct = `${window.percent.toFixed(0)}%`.padStart(4); + lines.push(` ${label.padEnd(5)} ${paint(window.percent, bar(window.percent))} ${pct} ${amount.padEnd(30)} ${reset}`); + if (window.exceeded) lines.push(' over the limit for this window'); + }; + + row('5h', view.windows.fiveHour, 'not applicable on this plan'); + row('week', view.windows.weekly, 'not applicable on this plan'); + row('month', view.monthly, 'unknown'); + + // The summary block is about the current billing period; the rows above are about + // the quota windows. Different questions, so they get different lines. + if (view.spend.requests || view.spend.total > 0) { + const parts = []; + if (view.spend.requests) parts.push(`${view.spend.requests} requests`); + if (view.spend.failed) parts.push(`${view.spend.failed} failed`); + if (view.spend.total > 0) parts.push(`${money(view.spend.total)} spent`); + if (view.spend.perRequest) parts.push(`avg ${price(view.spend.perRequest)} each`); + lines.push(` period ${parts.join(' / ')}`); + if (view.spend.perRequest) { + lines.push(` the month has room for about ${Math.floor(view.monthly.remaining / view.spend.perRequest)} more at that price`); + } + } + for (const error of view.errors) lines.push(` note: ${error}`); + return lines.join('\n'); +} + +/* ---------------------------------------------------------------------- CLI */ + +async function main(argv) { + const flags = new Set(argv.slice(2)); + if (flags.has('--help') || flags.has('-h')) { + console.log('usage: cc-usage.mjs [--json]\n\nPrints the Command Code 5-hour, weekly and monthly quota windows.'); + return 0; + } + + const credential = resolveCredential(); + if (!credential) { + console.error( + 'No Command Code credential found. Either set COMMAND_CODE_API_KEY, or sign in with the Command Code CLI so that ~/.commandcode/auth.json exists.' + ); + return 2; + } + + let raw; + try { + raw = await collectUsage(credential.apiKey); + } catch (error) { + console.error(error.message); + if (/HTTP 401|HTTP 403/.test(error.message)) { + console.error('The credential was rejected - sign in again with the Command Code CLI, or check the key.'); + } + return 1; + } + + const view = normalize(raw); + console.log(flags.has('--json') ? JSON.stringify(view, null, 2) : render(view)); + return 0; +} + +// Only run when invoked directly, so the file can also be imported. +const invokedDirectly = process.argv[1] && import.meta.url.endsWith(path.basename(process.argv[1])); +if (invokedDirectly) main(process.argv).then((code) => process.exit(code));