diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..5efa19b --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", + "name": "command-code-usage", + "description": "See your Command Code plan usage (5-hour and weekly rolling windows, monthly credits or balance) from inside your coding agent, with a remaining-requests estimate. Requires a Command Code plan.", + "owner": { + "name": "Jovan1666" + }, + "plugins": [ + { + "name": "command-code-usage", + "source": "./command-code-usage", + "version": "1.1.0", + "description": "Check your Command Code plan usage from inside your coding agent: 5-hour and weekly rolling windows, monthly credits or balance, plus a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", + "displayName": "Command Code Usage", + "category": "utilities", + "homepage": "https://github.com/Jovan1666/zcode-command-code-usage", + "author": { + "name": "Jovan1666", + "url": "https://github.com/Jovan1666" + } + } + ] +} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..d24667f --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,81 @@ +name: Check + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + # 三平台矩阵:插件声称跨平台,就必须真的在三平台上验过。 + # 本地只在 Windows 上验证过,Linux 与 macOS 由这里覆盖。 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 发布检查(清单一致性 / 版本 / 命名规则 / 密钥与个人路径) + run: node scripts/check.mjs + + - name: 离线功能冒烟测试 + shell: bash + run: | + set -e + S=command-code-usage/scripts/cc-usage.mjs + node "$S" --help > /dev/null + node "$S" --demo > /dev/null + node "$S" --demo hot > /dev/null + node "$S" --demo --md > /dev/null + node "$S" --demo --compact > /dev/null + node "$S" --demo --json > /dev/null + echo "全部输出模式通过(--demo 不联网)" + + - name: 安装到临时 HOME 并验证命令可被发现 + shell: bash + env: + # POSIX 上 Node 读 HOME,Windows 上读 USERPROFILE;两个都设,覆盖两平台。 + HOME: ${{ runner.temp }}/fakehome + USERPROFILE: ${{ runner.temp }}/fakehome + run: | + set -e + mkdir -p "$HOME" + node command-code-usage/scripts/install-user-scope.mjs + node command-code-usage/scripts/verify-discoverable.cjs . + + - name: 安装器冲突保护(不得覆盖用户自己的同名文件) + shell: bash + env: + HOME: ${{ runner.temp }}/fakehome2 + USERPROFILE: ${{ runner.temp }}/fakehome2 + run: | + set -e + mkdir -p "$HOME/.zcode/commands" + echo "我自己的命令" > "$HOME/.zcode/commands/quota.md" + if node command-code-usage/scripts/install-user-scope.mjs; then + echo "::error::安装器在存在同名外部文件时应当中止" + exit 1 + fi + grep -q "我自己的命令" "$HOME/.zcode/commands/quota.md" + echo "冲突保护生效:用户文件未被覆盖" + + - name: 安装器可卸载 + shell: bash + env: + HOME: ${{ runner.temp }}/fakehome + USERPROFILE: ${{ runner.temp }}/fakehome + run: | + set -e + node command-code-usage/scripts/install-user-scope.mjs --uninstall + test ! -e "$HOME/.zcode/commands/quota.md" + echo "卸载干净" diff --git a/CHANGELOG.md b/CHANGELOG.md index b669d39..47f4adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project are documented here. This project follows [Semantic Versioning](https://semver.org/). +## [1.1.0] — 2026-09-21 + +Cross-agent support, so the plugin is not tied to one editor. + +### Added +- **Claude Code support.** The plugin now ships a `.claude-plugin/plugin.json` alongside the ZCode + manifest, and a strict-clean `.claude-plugin/marketplace.json` that passes + `claude plugin validate --strict` with zero warnings. +- **Cross-platform CI** (`.github/workflows/check.yml`) running on Ubuntu, Windows and macOS: the + release gate, offline smoke tests for every output mode, an install-and-discover check against a + throwaway home directory, and the installer's conflict guard. +- **`scripts/check.mjs`** — a release gate shared by CI and local runs. It compares the duplicated + fields between the ZCode and Claude manifests, enforces version consistency across all five + places that carry a version, checks the command/skill files against ZCode's actual parsing rules + (name pattern, allowed frontmatter keys, reserved command names), and fails on any leaked secret + or machine-specific path. + +### Changed +- The command body's script lookup now searches the known agent directories — `~/.zcode`, + `~/.claude`, `~/.agents`, `~/.codex` — instead of only ZCode's, so the same command works + wherever the plugin was installed from. + ## [1.0.0] — 2026-09-21 First public release. diff --git a/README.md b/README.md index 7ddbef4..d273bb7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# Command Code Usage — a ZCode plugin +# Command Code Usage -[English](README.md) · [简体中文](README.zh-CN.md) +[English](README.md) · [简体中文](README.zh-CN.md) · [![Check](https://github.com/Jovan1666/zcode-command-code-usage/actions/workflows/check.yml/badge.svg)](https://github.com/Jovan1666/zcode-command-code-usage/actions/workflows/check.yml) -Check how much of your **Command Code** plan you have left, without leaving the ZCode conversation. +Check how much of your **Command Code** plan you have left, without leaving the conversation. +Works in **ZCode** and **Claude Code**. Command Code plans (Go / GOAT / Pro / Max / Teams) pace your monthly credits with two **rolling windows**: a 5-hour cap and a weekly cap. A window opens on your first request and resets a fixed @@ -54,7 +55,9 @@ plugin puts the panel. No browser tab, no website to poll. ## Requirements -- **ZCode desktop** (any recent version; the plugin needs `commands` + `skills` support). +- **An agent that supports commands and skills** — ZCode or Claude Code. Both are verified; the + layout is the standard `.zcode-plugin` / `.claude-plugin` one, so other compatible agents should + work too. - **A Command Code plan.** Without one the panel has nothing to show. If you are on pay-as-you-go rather than a subscription, it still works but shows a balance instead of windows. - **Node.js** — only to run the bundled scripts. They use nothing but Node built-ins, so there is no @@ -62,9 +65,9 @@ plugin puts the panel. No browser tab, no website to poll. ## Install -### Option A — from the marketplace (recommended) +### ZCode — from the marketplace (recommended) -1. In ZCode open **Plugin Marketplace → Add → Add Plugin Marketplace**. +1. Open **Plugin Marketplace → Add → Add Plugin Marketplace**. 2. Paste this repository: ``` @@ -74,7 +77,14 @@ plugin puts the panel. No browser tab, no website to poll. 3. Go to **Personal → Command Code Usage → Install**. 4. **Fully quit and reopen ZCode**, then start a new task and run `/quota`. -### Option B — local install script +### Claude Code + +``` +/plugin marketplace add Jovan1666/zcode-command-code-usage +/plugin install command-code-usage@command-code-usage +``` + +### Option B — local install script (ZCode, no marketplace needed) Clone the repo, then: @@ -98,11 +108,11 @@ only updates a file that is both in its own install manifest *and* unchanged sin same-named command you wrote yourself, or a file you edited afterwards, is refused with a reason instead of being clobbered. -> **Do not use both options at once.** User-scope copies are discovered before plugin-provided ones, -> so a local copy shadows the marketplace version and the Update button stops affecting your -> commands. Run `--uninstall` before switching to the marketplace route. +> **Do not use both a local install and the marketplace route at once.** User-scope copies are +> discovered before plugin-provided ones, so a local copy shadows the marketplace version and the +> Update button stops affecting your commands. Run `--uninstall` before switching. -### After installing: restart ZCode +### After installing in ZCode: restart the app ZCode snapshots the command and skill catalogue when a session starts. A plugin directory created while the app is running is not picked up by merely opening a new task — and since ZCode keeps a @@ -225,11 +235,15 @@ rewind, skill, target, variant`. ``` . -├── marketplace.json marketplace catalogue (repo root = marketplace root) +├── marketplace.json ZCode-catalogue (repo root = marketplace root) +├── .claude-plugin/marketplace.json Claude-catalogue (identical shared fields, strict-clean) ├── README.md / README.zh-CN.md -├── LICENSE +├── LICENSE / CHANGELOG.md +├── .github/workflows/check.yml CI: 3 platforms, release checks, offline smoke tests +├── scripts/check.mjs release gate (shared with CI) └── command-code-usage/ the plugin - ├── .zcode-plugin/plugin.json + ├── .zcode-plugin/plugin.json manifest read by ZCode (checked first) + ├── .claude-plugin/plugin.json manifest read by Claude Code ├── commands/ │ ├── quota.md /quota │ └── usage.md /usage @@ -240,21 +254,54 @@ rewind, skill, target, variant`. └── verify-discoverable.cjs diagnostic: re-implements ZCode's command parser ``` -`commands/*.md` contain the token `@@CC_USAGE_SCRIPT@@`, which the installer substitutes with the -absolute path of `cc-usage.mjs` at install time. When the plugin is installed from the marketplace -instead, that token is left as-is and the shell snippet falls back to locating the script — so both -routes work. +### Why some files exist twice + +The plugin and the catalogue each have a ZCode copy and a Claude Code copy, because the two +ecosystems look in different places and accept different fields: + +- ZCode reads `.zcode-plugin/plugin.json` first, then falls back to `.claude-plugin/`. + Claude Code only reads `.claude-plugin/`. +- ZCode's catalogue accepts presentational fields Claude Code ignores — + `displayName_i18n`, `description_i18n`, `examplePrompts`, `examplePrompts_i18n`. Claude Code's + validator reports those as unknown fields, and fails under `--strict`. + +So the ZCode catalogue keeps the localized display names (its users see Chinese labels), and the +Claude catalogue stays strict-clean so it passes the validator the review pipeline runs. Everything +the two share — name, version, description, source, category, homepage, author — is identical, and +`scripts/check.mjs` fails the build if that ever drifts. Run it before committing: + +```bash +node scripts/check.mjs +``` + +`commands/*.md` contain the token `@@CC_USAGE_SCRIPT@@`. The install script substitutes it with the +absolute path of `cc-usage.mjs`; when the plugin arrives through either marketplace the token is left +alone and the shell snippet searches the known agent directories (`~/.zcode`, `~/.claude`, +`~/.agents`, `~/.codex`) for the script instead — so every route works. + +## Distribution status + +| Channel | State | +|---|---| +| **This repo added as a marketplace** | **Live.** Paste `Jovan1666/zcode-command-code-usage` in either agent. | +| ZCode official marketplace (`zcode-plugins-official`) | Not submitted. Its description says it carries community plugins, but Z.ai exposes no public submission process — there is no form, and nothing in the app to submit through. | +| Claude Code community marketplace (`anthropics/claude-plugins-community`) | Not submitted. That repo is a read-only mirror; submissions go through a Console form that needs an account login, so it is a maintainer action, not something CI or a script can do. The official marketplace is invitation-only: its docs state there is no application process. | ## Status and scope -Tested on Windows with the ZCode desktop app: the API integration, all output modes (terminal, -`--md`, `--compact`, `--json`, `--from-json`, HTML, serve), the account-shape branches, credential -resolution, every error path, and the installer's conflict handling (fresh install, re-install, -edited file, foreign file). +Tested on Windows: the API integration, all output modes (terminal, `--md`, `--compact`, `--json`, +`--from-json`, HTML, serve), the account-shape branches, credential resolution, every error path, +and the installer's conflict handling (fresh install, re-install, edited file, foreign file). +A clean-machine install from the published repo was also exercised end to end (clone → install → +discovery → execution). + +CI additionally runs the release gate, the offline smoke tests, an install-and-discover check, and +the installer's conflict guard on **Ubuntu, Windows and macOS**. -Installing and running the command inside a live ZCode session is exercised by the maintainer; a -clean-machine verification by a second user has not been done. If something misbehaves, please open -an issue with the output of `verify-discoverable.cjs .` and the exact message you saw. +Not yet verified: a second person installing through a marketplace UI, and running the commands +inside a live Claude Code session. If something misbehaves, please open an issue with the output of +`node scripts/check.mjs` (or `command-code-usage/scripts/verify-discoverable.cjs .` for discovery +problems) and the exact message you saw. Not affiliated with Command Code. It reads your own account's usage through the same endpoints the official CLI uses; it does not proxy, modify or transmit anything anywhere else. diff --git a/README.zh-CN.md b/README.zh-CN.md index a87c7b1..505910e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,8 +1,9 @@ -# Command Code Usage —— ZCode 插件 +# Command Code Usage -[English](README.md) · [简体中文](README.zh-CN.md) +[English](README.md) · [简体中文](README.zh-CN.md) · [![Check](https://github.com/Jovan1666/zcode-command-code-usage/actions/workflows/check.yml/badge.svg)](https://github.com/Jovan1666/zcode-command-code-usage/actions/workflows/check.yml) -在 ZCode 对话里直接看 **Command Code** 套餐还剩多少用量,不用离开对话、不用开网页。 +在对话里直接看 **Command Code** 套餐还剩多少用量,不用离开对话、不用开网页。 +**ZCode 与 Claude Code 都支持。** Command Code 的套餐(Go / GOAT / Pro / Max / Teams)除了月度额度,还压着两个**滚动窗口**: 5 小时上限和每周上限。窗口从你第一次请求开始计时,到点重置,**不跟自然日/周走**,用量也不跨 @@ -51,14 +52,15 @@ ZCode 没有给插件留常驻显示位,这不是猜的,是它自己的代 ## 环境要求 -- **ZCode 桌面端**(需要支持 `commands` 与 `skills` 的版本)。 +- **一个支持命令与技能的 agent** —— ZCode 或 Claude Code,两者都已验证;清单与目录用的是 + 标准布局(`.zcode-plugin` / `.claude-plugin`),其他兼容的 agent 应该也能用。 - **一个 Command Code 套餐。** 没有套餐就没数可读。如果你是按量计费而非订阅,插件照样能用, 只是显示余额而不是窗口。 - **Node.js** —— 只用来跑插件自带的脚本。脚本只用 Node 内置模块,**不需要 `npm install`**。 ## 安装 -### 方式 A:从插件市场装(推荐) +### ZCode:从插件市场装(推荐) 1. ZCode 里打开 **插件市场 → 添加 → 添加插件市场**。 2. 粘贴本仓库: @@ -70,7 +72,14 @@ ZCode 没有给插件留常驻显示位,这不是猜的,是它自己的代 3. 到 **个人 → Command Code Usage → 安装**。 4. **完全退出 ZCode 再打开**,然后新建任务,输入 `/quota`。 -### 方式 B:本地安装脚本 +### Claude Code + +``` +/plugin marketplace add Jovan1666/zcode-command-code-usage +/plugin install command-code-usage@command-code-usage +``` + +### 方式 B:本地安装脚本(ZCode,不走市场) 先克隆仓库,然后: @@ -92,10 +101,10 @@ node command-code-usage/scripts/verify-discoverable.cjs . # 验证 ZC 安装器**绝不覆盖你自己的内容**:它记录每次写入的哈希,只有「在自己安装清单里、且自写入后没被 改动过」的文件才更新。你自己写的同名命令、或你事后改过的文件,都会被拒绝并说明原因,而不是被清掉。 -> **两种方式别同时用。** 用户级副本的发现优先级高于插件,同时存在时本地副本会遮蔽市场版本, -> 市场的「更新」按钮对你的命令就不生效了。要切到市场方式,先跑一次 `--uninstall`。 +> **本地安装与市场安装别同时用。** 用户级副本的发现优先级高于插件,同时存在时本地副本会遮蔽市场 +> 版本,市场的「更新」按钮对你的命令就不生效了。要切到市场方式,先跑一次 `--uninstall`。 -### 装完之后:重启 ZCode +### 在 ZCode 里装完之后:重启应用 ZCode 在**会话启动时**对命令与技能清单做快照。应用运行期间新建的插件目录,光靠「新建任务」是 读不到的——而且 ZCode 有托盘图标,关窗口往往只是最小化、进程还活着。**要完全退出** @@ -211,11 +220,15 @@ mcp, mode, model, new, plan, plugin, plugins, resume, rewind, skill, target, var ``` . -├── marketplace.json 市场清单(仓库根目录即市场根目录) +├── marketplace.json ZCode 侧市场清单(仓库根目录即市场根目录) +├── .claude-plugin/marketplace.json Claude 侧市场清单(共享字段完全一致,strict 零警告) ├── README.md / README.zh-CN.md -├── LICENSE +├── LICENSE / CHANGELOG.md +├── .github/workflows/check.yml CI:三平台矩阵 + 发布检查 + 离线冒烟 +├── scripts/check.mjs 发布门禁(CI 与本地共用同一个脚本) └── command-code-usage/ 插件本体 - ├── .zcode-plugin/plugin.json + ├── .zcode-plugin/plugin.json ZCode 读这个(优先) + ├── .claude-plugin/plugin.json Claude Code 读这个 ├── commands/ │ ├── quota.md /quota │ └── usage.md /usage @@ -226,18 +239,50 @@ mcp, mode, model, new, plan, plugin, plugins, resume, rewind, skill, target, var └── verify-discoverable.cjs 诊断:复刻 ZCode 的命令解析器 ``` +### 为什么有些文件有两份 + +插件清单和市场清单各有一个 ZCode 副本、一个 Claude Code 副本——因为两个生态看的位置不同、 +接受的字段也不同: + +- ZCode 先读 `.zcode-plugin/plugin.json`,读不到才回退到 `.claude-plugin/`;Claude Code 只读 + `.claude-plugin/`。 +- ZCode 的市场条目支持 `displayName_i18n`、`description_i18n`、`examplePrompts`、`examplePrompts_i18n` + 这些展示字段,而 Claude Code 会把这些视为未知字段并给出警告,`--strict` 下直接失败。 + +所以 ZCode 那份保留了本地化显示名(中文用户看到中文标签),Claude 那份保持 strict 零警告, +以便通过审核流水线跑的同一个校验。两份共有的字段——name、version、description、source、 +category、homepage、author——完全一致,一旦漂移 `scripts/check.mjs` 会让构建失败。提交前跑: + +```bash +node scripts/check.mjs +``` + `commands/*.md` 里含有 `@@CC_USAGE_SCRIPT@@` 这个占位符。用安装脚本装时,它会被替换成 -`cc-usage.mjs` 的绝对路径;从市场安装时占位符保持原样,命令正文里的 shell 片段会自动回退到 -探测脚本位置——所以两条安装路线都能用。 +`cc-usage.mjs` 的绝对路径;走任一市场安装时占位符保持原样,命令正文里的 shell 片段会自动到 +各 agent 的已知目录(`~/.zcode`、`~/.claude`、`~/.agents`、`~/.codex`)里找脚本——所以每条 +安装路线都能用。 + +## 分发状态 + +| 渠道 | 状态 | +|---|---| +| **把本仓库添加为市场** | **已可用。** 在任一 agent 里粘贴 `Jovan1666/zcode-command-code-usage`。 | +| ZCode 官方市场(`zcode-plugins-official`) | 未投稿。它的描述写着收录社区插件,但 Z.ai 没有公开的投稿流程——没有表单,应用里也没有可提交的入口。 | +| Claude Code 社区市场(`anthropics/claude-plugins-community`) | 未投稿。那个仓库是只读镜像,投稿走 Console 表单、需要账号登录,所以这是维护者的动作,CI 或脚本做不到。官方市场是邀请制:文档明确写了没有申请流程。 | ## 状态与范围 -已在 Windows + ZCode 桌面端验证:接口取数、全部输出模式(终端、`--md`、`--compact`、`--json`、 +已在 Windows 上验证:接口取数、全部输出模式(终端、`--md`、`--compact`、`--json`、 `--from-json`、HTML、serve)、各账号形态分支、凭证解析、所有错误路径,以及安装器的冲突处理 -(全新安装、重复安装、被改过的文件、外来同名文件)。 +(全新安装、重复安装、被改过的文件、外来同名文件)。另外还做过一次从已发布仓库出发的 +干净环境端到端验证(克隆 → 安装 → 发现 → 执行)。 + +CI 另外在 **Ubuntu、Windows、macOS** 三个平台跑发布门禁、离线冒烟、安装并发现校验, +以及安装器的冲突保护。 -在真实 ZCode 会话里安装并执行命令,目前是维护者本机验证;**尚未做过第二人在干净机器上的验证**。 -如果遇到问题,请带上 `verify-discoverable.cjs .` 的输出和你看到的确切报错开 issue。 +尚未验证:第二个人通过市场界面安装,以及在真实 Claude Code 会话里执行命令。 +如果遇到问题,请带上 `node scripts/check.mjs` 的输出(命令发现类问题用 +`command-code-usage/scripts/verify-discoverable.cjs .`)和你看到的确切报错开 issue。 与 Command Code 官方无关。它通过官方 CLI 使用的同一批端点读取你自己账号的用量, 不做代理、不修改、也不向其他任何地方传输数据。 diff --git a/command-code-usage/.claude-plugin/plugin.json b/command-code-usage/.claude-plugin/plugin.json new file mode 100644 index 0000000..f02843a --- /dev/null +++ b/command-code-usage/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "command-code-usage", + "version": "1.1.0", + "description": "Check your Command Code plan usage from inside your coding agent: 5-hour and weekly rolling windows, monthly credits or balance, plus a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", + "author": { + "name": "Jovan1666", + "url": "https://github.com/Jovan1666" + }, + "license": "MIT", + "homepage": "https://github.com/Jovan1666/zcode-command-code-usage", + "repository": "https://github.com/Jovan1666/zcode-command-code-usage", + "commands": "./commands", + "skills": "./skills" +} diff --git a/command-code-usage/.zcode-plugin/plugin.json b/command-code-usage/.zcode-plugin/plugin.json index 29e0a60..f02843a 100644 --- a/command-code-usage/.zcode-plugin/plugin.json +++ b/command-code-usage/.zcode-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "command-code-usage", - "version": "1.0.0", - "description": "Check your Command Code plan usage from inside ZCode: 5-hour and weekly rolling windows, monthly credits or balance, remaining-requests estimate and burn-rate warning.", + "version": "1.1.0", + "description": "Check your Command Code plan usage from inside your coding agent: 5-hour and weekly rolling windows, monthly credits or balance, plus a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", "author": { "name": "Jovan1666", "url": "https://github.com/Jovan1666" diff --git a/command-code-usage/commands/quota.md b/command-code-usage/commands/quota.md index 6d590ad..8c36b75 100644 --- a/command-code-usage/commands/quota.md +++ b/command-code-usage/commands/quota.md @@ -5,11 +5,17 @@ argument-hint: "[--md | --compact | --json | --verbose | --demo hot]" 查看 Command Code 额度用量。 -执行下面这段(已包含脚本定位,直接整段运行即可): +执行下面整段(已包含脚本定位,直接整段运行即可): ```bash CC_SCRIPT="@@CC_USAGE_SCRIPT@@" -[ -f "$CC_SCRIPT" ] || CC_SCRIPT=$(find "${HOME:-$USERPROFILE}/.zcode" -type f -name cc-usage.mjs -path '*command-code-usage*' -print -quit 2>/dev/null) +if [ ! -f "$CC_SCRIPT" ]; then + for d in "$HOME/.zcode" "$HOME/.claude" "$HOME/.agents" "$HOME/.codex"; do + CC_SCRIPT=$(find "$d" -type f -name cc-usage.mjs -print -quit 2>/dev/null) + [ -n "$CC_SCRIPT" ] && break + done +fi +[ -f "$CC_SCRIPT" ] || { echo "找不到 cc-usage.mjs,插件可能未正确安装或已被移动。"; exit 2; } node "$CC_SCRIPT" $ARGUMENTS ``` diff --git a/command-code-usage/commands/usage.md b/command-code-usage/commands/usage.md index a36bbe9..8519470 100644 --- a/command-code-usage/commands/usage.md +++ b/command-code-usage/commands/usage.md @@ -5,11 +5,17 @@ argument-hint: "[--md | --compact | --json | --verbose | --demo hot]" 查看 Command Code 额度用量(`/quota` 的等价写法)。 -执行下面这段(已包含脚本定位,直接整段运行即可): +执行下面整段(已包含脚本定位,直接整段运行即可): ```bash CC_SCRIPT="@@CC_USAGE_SCRIPT@@" -[ -f "$CC_SCRIPT" ] || CC_SCRIPT=$(find "${HOME:-$USERPROFILE}/.zcode" -type f -name cc-usage.mjs -path '*command-code-usage*' -print -quit 2>/dev/null) +if [ ! -f "$CC_SCRIPT" ]; then + for d in "$HOME/.zcode" "$HOME/.claude" "$HOME/.agents" "$HOME/.codex"; do + CC_SCRIPT=$(find "$d" -type f -name cc-usage.mjs -print -quit 2>/dev/null) + [ -n "$CC_SCRIPT" ] && break + done +fi +[ -f "$CC_SCRIPT" ] || { echo "找不到 cc-usage.mjs,插件可能未正确安装或已被移动。"; exit 2; } node "$CC_SCRIPT" $ARGUMENTS ``` diff --git a/command-code-usage/scripts/cc-usage.mjs b/command-code-usage/scripts/cc-usage.mjs index 16418fc..c09eaed 100644 --- a/command-code-usage/scripts/cc-usage.mjs +++ b/command-code-usage/scripts/cc-usage.mjs @@ -29,7 +29,7 @@ import path from 'node:path'; import http from 'node:http'; import { exec } from 'node:child_process'; -const VERSION = '1.0.0'; +const VERSION = '1.1.0'; const DEFAULT_API_BASE = 'https://api.commandcode.ai'; const PROVIDER_MATCH = /commandcode\.ai/i; diff --git a/marketplace.json b/marketplace.json index f2c6f02..347ea0d 100644 --- a/marketplace.json +++ b/marketplace.json @@ -4,8 +4,8 @@ { "name": "command-code-usage", "source": "./command-code-usage", - "version": "1.0.0", - "description": "See your Command Code plan usage (5-hour and weekly rolling windows, monthly credits or balance) right inside the ZCode conversation, with a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", + "version": "1.1.0", + "description": "Check your Command Code plan usage from inside your coding agent: 5-hour and weekly rolling windows, monthly credits or balance, plus a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", "displayName": "Command Code Usage", "displayName_i18n": { "zh-CN": "Command Code 额度面板" @@ -30,5 +30,6 @@ ] } } - ] + ], + "description": "See your Command Code plan usage (5-hour and weekly rolling windows, monthly credits or balance) from inside your coding agent, with a remaining-requests estimate. Requires a Command Code plan." } diff --git a/scripts/check.mjs b/scripts/check.mjs new file mode 100644 index 0000000..432ebe4 --- /dev/null +++ b/scripts/check.mjs @@ -0,0 +1,220 @@ +#!/usr/bin/env node +/** + * 发布前检查 —— 仓库自带的门禁,本地和 CI 跑的是同一个脚本。 + * + * node scripts/check.mjs + * + * 覆盖两类风险: + * 1. 双生态清单漂移 —— 插件同时提供 .zcode-plugin/ 与 .claude-plugin/ 清单, + * 市场同时提供根目录与 .claude-plugin/ 清单;重复字段必须完全一致。 + * 2. 发布事故 —— 版本号不一致、遗留个人路径/密钥、命令名或 frontmatter + * 不符合 ZCode 的解析规则。 + * + * 只用 Node 内置模块。退出码 0 = 通过。 + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const PLUGIN = path.join(ROOT, 'command-code-usage'); + +let failures = 0; +let checks = 0; + +function ok(cond, label, detail) { + checks++; + if (cond) { + console.log(` \u2714 ${label}`); + } else { + failures++; + console.log(` \u2718 ${label}${detail ? `\n ${detail}` : ''}`); + } +} + +function section(name) { + console.log(`\n${name}`); +} + +function readJson(rel) { + try { + return JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8')); + } catch (err) { + failures++; + checks++; + console.log(` \u2718 ${rel}: 不是合法 JSON\n ${err.message}`); + return null; + } +} + +/* ------------------------------------------------------------ 1. JSON 有效性 */ + +section('JSON 有效性'); +const zPlugin = readJson('command-code-usage/.zcode-plugin/plugin.json'); +const cPlugin = readJson('command-code-usage/.claude-plugin/plugin.json'); +const zMarket = readJson('marketplace.json'); +const cMarket = readJson('.claude-plugin/marketplace.json'); +if (!zPlugin || !cPlugin || !zMarket || !cMarket) { + console.log('\n关键清单缺失或损坏,后续检查已跳过。'); + process.exit(1); +} +ok(true, '四个清单文件均可解析'); + +/* ------------------------------------------------------------ 2. 版本一致性 */ + +section('版本一致性'); +const srcVersion = /const VERSION = '([0-9.]+)'/.exec( + fs.readFileSync(path.join(PLUGIN, 'scripts', 'cc-usage.mjs'), 'utf8'), +)?.[1]; +const versions = { + '.zcode-plugin/plugin.json': zPlugin.version, + '.claude-plugin/plugin.json': cPlugin.version, + 'marketplace.json': zMarket.plugins[0].version, + '.claude-plugin/marketplace.json': cMarket.plugins[0].version, + 'cc-usage.mjs VERSION': srcVersion, +}; +for (const [k, v] of Object.entries(versions)) console.log(` ${v} ${k}`); +ok(new Set(Object.values(versions)).size === 1, '五处版本号完全一致'); + +/* --------------------------------------------------- 3. 双生态清单重复字段 */ + +section('双生态清单一致性(重复字段必须相同)'); +const pluginFields = ['name', 'version', 'description', 'author', 'license', 'homepage', 'repository']; +for (const f of pluginFields) { + const same = JSON.stringify(zPlugin[f]) === JSON.stringify(cPlugin[f]); + ok(same, `插件清单 .${f} 一致`, same ? '' : `.zcode-plugin=${JSON.stringify(zPlugin[f])}\n .claude-plugin=${JSON.stringify(cPlugin[f])}`); +} +const entryFields = ['name', 'source', 'version', 'description', 'displayName', 'category', 'homepage', 'author']; +for (const f of entryFields) { + const a = zMarket.plugins[0][f]; + const b = cMarket.plugins[0][f]; + const same = JSON.stringify(a) === JSON.stringify(b); + ok(same, `市场条目 .${f} 一致`, same ? '' : `root=${JSON.stringify(a)}\n .claude-plugin=${JSON.stringify(b)}`); +} +ok(zMarket.name === cMarket.name, '两个市场文件的市场名一致'); + +/* ------------------------------------------------------------ 4. ZCode 规则 */ + +section('ZCode 清单规则'); +const NAME_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/; +ok(NAME_RE.test(zPlugin.name), `插件名符合 ZCode 规则 (${zPlugin.name})`); +ok(NAME_RE.test(zMarket.name), `市场名符合 ZCode 规则 (${zMarket.name})`); +ok(path.basename(PLUGIN) === zPlugin.name, '插件目录名 == 清单里的 name'); +ok(zMarket.plugins[0].name === zPlugin.name, '市场条目名 == 插件名'); + +const source = zMarket.plugins[0].source; +const resolved = path.resolve(ROOT, source); +ok(resolved.startsWith(ROOT + path.sep), `marketplace source 未逃出仓库根 (${source})`); +ok(fs.existsSync(path.join(resolved, '.zcode-plugin', 'plugin.json')), 'source 指向真实插件'); + +// 这几个字段 ZCode「只认不执行」,写进去会让使用者以为生效了 +const inert = ['channels', 'lspServers', 'outputStyles', 'settings'].filter((k) => k in zPlugin); +ok(inert.length === 0, '插件清单不含 ZCode 只认不执行的字段', inert.join(', ')); + +// ZCode 实际支持的抽屉字段,写错会静默失效 +const zcodeEntryAllowed = new Set([ + 'name', 'source', 'version', 'description', 'displayName', 'displayName_i18n', 'description_i18n', + 'icon', 'category', 'homepage', 'privacyPolicy', 'termsOfService', 'heroImage', 'author', + 'examplePrompts', 'examplePrompts_i18n', 'requiresPaidPlan', +]); +const unknownZ = Object.keys(zMarket.plugins[0]).filter((k) => !zcodeEntryAllowed.has(k)); +ok(unknownZ.length === 0, 'ZCode 市场条目字段均受支持', unknownZ.join(', ')); + +/* --------------------------------------------------- 5. 命令与技能的可发现性 */ + +section('命令与技能(按 ZCode 解析规则)'); +const CMD_NAME_RE = /^[a-z0-9][a-z0-9_:-]{0,63}$/; +const CMD_KEYS = new Set(['allowed-tools', 'argument-hint', 'description', 'disable-noninteractive', 'model', 'skills']); +// ZCode 保留名(内置命令 + 别名 + compress/plan);命中会被静默丢弃 +const RESERVED = new Set(['clear', 'compact', 'compress', 'continue', 'dwf', 'effort', 'expert', 'fork', 'goal', + 'help', 'init', 'language', 'locale', 'login', 'logout', 'mcp', 'mode', 'model', 'new', 'plan', 'plugin', + 'plugins', 'resume', 'rewind', 'skill', 'target', 'variant']); + +const cmdDir = path.join(PLUGIN, 'commands'); +const cmdFiles = fs.existsSync(cmdDir) ? fs.readdirSync(cmdDir).filter((f) => f.endsWith('.md')) : []; +ok(cmdFiles.length > 0, '至少有一个命令文件'); +for (const f of cmdFiles) { + const stem = f.replace(/\.md$/, ''); + const text = fs.readFileSync(path.join(cmdDir, f), 'utf8'); + ok(CMD_NAME_RE.test(stem), `${stem}: 命令名合法`); + ok(!RESERVED.has(stem.toLowerCase()), `${stem}: 不与 ZCode 保留名冲突`); + const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(text); + ok(!!m, `${stem}: frontmatter 结构完整`); + if (!m) continue; + const lines = m[1].split(/\r?\n/); + const indented = lines.filter((l) => /^\s/.test(l)); + ok(indented.length === 0, `${stem}: frontmatter 无缩进行`); + const keys = lines.filter((l) => l.includes(':')).map((l) => l.split(':')[0].trim()); + const bad = keys.filter((k) => !CMD_KEYS.has(k)); + ok(bad.length === 0, `${stem}: frontmatter 键均有效`, bad.join(', ')); + ok(m[2].trim().length > 0, `${stem}: 正文非空`); + ok(keys.includes('description'), `${stem}: 有 description`); +} + +const skillDir = path.join(PLUGIN, 'skills'); +const skills = fs.existsSync(skillDir) + ? fs.readdirSync(skillDir).filter((d) => fs.existsSync(path.join(skillDir, d, 'SKILL.md'))) + : []; +ok(skills.length > 0, '至少有一个技能'); +for (const s of skills) { + const text = fs.readFileSync(path.join(skillDir, s, 'SKILL.md'), 'utf8'); + const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/.exec(text); + ok(!!m, `${s}: SKILL.md frontmatter 完整`); + if (!m) continue; + const nm = /^name:\s*(\S+)/m.exec(m[1])?.[1]; + ok(nm === s, `${s}: skill name 与目录名一致`, nm); + ok(/^description:\s*\S/m.test(m[1]), `${s}: 有 description`); +} + +/* --------------------------------------------------------------- 6. 卫生检查 */ + +section('发布卫生'); +const filesToScan = []; +(function walk(dir) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + if (e.name === '.git' || e.name === 'node_modules') continue; + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (/\.(md|json|mjs|cjs|txt|yml|yaml)$/.test(e.name)) filesToScan.push(p); + } +})(ROOT); + +const SECRET_PATTERNS = [ + [/user_[A-Za-z0-9]{40,}/, 'Command Code API key'], + [/github_pat_[A-Za-z0-9_]{20,}/, 'GitHub PAT'], + [/\bsk-[A-Za-z0-9]{20,}/, 'OpenAI 风格 key'], + [/Bearer\s+[A-Za-z0-9_-]{20,}/, '字面 Bearer 令牌'], +]; +let secretHits = []; +for (const f of filesToScan) { + const t = fs.readFileSync(f, 'utf8'); + for (const [re, label] of SECRET_PATTERNS) { + if (re.test(t)) secretHits.push(`${path.relative(ROOT, f)} (${label})`); + } +} +ok(secretHits.length === 0, '无密钥字面量', secretHits.join('\n ')); + +// 本机路径与个人邮箱:只允许 example.com +let pathHits = []; +let emailHits = []; +for (const f of filesToScan) { + const t = fs.readFileSync(f, 'utf8'); + const rel = path.relative(ROOT, f); + if (/Users[\\/](admin|Jovan)\b/i.test(t) || /\/c\/Users\//.test(t)) pathHits.push(rel); + for (const m of t.matchAll(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g)) { + if (!/example\.(com|org|net)$/.test(m[0])) emailHits.push(`${rel}: ${m[0]}`); + } +} +ok(pathHits.length === 0, '无本机绝对路径残留', pathHits.join('\n ')); +ok(emailHits.length === 0, '无 example.com 之外的邮箱', emailHits.join('\n ')); + +/* ----------------------------------------------------------------- 汇总 */ + +section('结果'); +console.log(` 检查项 ${checks},失败 ${failures}`); +if (failures > 0) { + console.log('\n发布检查未通过。'); + process.exit(1); +} +console.log('\n全部通过。');