Skip to content

Repository files navigation

[upstream statusline lines]
Usage: ▓▓▓▓░░░░ 40% (1h27m🕗 5h) · ▓▓░░░░░░ 20% (4d3h🕔 7d)    # Tokeplan
Balance: ¥110.00 · $3.5                                        # Balance

tokenplan-usage-hud

License Tag Stars

A provider-agnostic Claude Code statusline plugin for token-plan usage / remaining quota. It picks what to render from ANTHROPIC_BASE_URL, so the same plugin works against any supported provider's plan endpoint — no per-provider re-install. Currently supported:

  • MiniMaxUsage: … / Remain: … (5-hour + weekly windows), from /v1/token_plan/remains
  • DeepSeekBalance: … (account balance), from /user/balance

For vanilla Anthropic, OpenRouter, or any other provider not on the list above, the plugin hides itself and passes any chained upstream statusline through unchanged.

We deliberately don't reimplement the kitchen-sink statuslines that already exist for vanilla Anthropic — claude-hud and ccstatusline cover that. This plugin is only the plan / quota piece that's provider-specific.

ANSI colors are 5-band (256-color SGR): bright green / dark green / yellow / orange / red. Applied to the displayed value + the colored bar segment; the empty part of the bar stays uncolored so it remains readable.

What's new

  • v0.4.0 (in development) — exposes 16 new statusline modules reading the captured Claude Code stdin payload: session identity (m_session, m_model, m_effort, m_repo, m_ccVersion), session metrics (m_sessionDuration, m_sessionApiDuration, m_linesAdded, m_linesRemoved), cumulative token counters (m_tokenInTotal, m_tokenOutTotal), context-window stats (m_contextSize, m_contextUsed, m_windowContext). Also: m_tokenIn / m_tokenOut and m_tokenInSpeed / m_tokenOutSpeed now read the per-turn current_usage fields instead of session-cumulative. BREAKING: the lineTemplate: { plan, balance } config field is removed — replaced by lineTemplates (registry of reusable fragments) + statuslineTemplate (the rendered template). See CHANGELOG.md for the full v0.4.0 list and the Upgrading to v0.4.0 section below for the migration recipe.

Install

The plugin is a single-plugin marketplace. Install it in three steps:

/plugin marketplace add cwf818/tokenplan-usage-hud
/plugin install tokenplan-usage-hud@tokenplan-usage-hud

After the plugin install, run /reload-plugins so the loader picks up the new commands before wiring it into settings.json. Forgetting this step is the most common cause of "command not found" right after install.

Then wire it into settings.json:

/tokenplan-usage-hud:install

This patches the active settings.json (user-level by default; pass --project for project-level):

  1. If statusLine is already managed by us (_tokenplan_managed: true), the command is a no-op.
  2. Otherwise, the current settings.json is backed up to settings.json.bak.<ISO-timestamp>.
  3. The original statusLine.command is preserved at <claude-root>/plugins/tokenplan-usage-hud/state/upstream-cmd.sh and <claude-root>/plugins/tokenplan-usage-hud/state/upstream-cmd.txt — sibling of config.json, stable across /plugin install rolls and cache wipes.
  4. The statusLine is rewritten to invoke our wrapper, which sets TOKENPLAN_UPSTREAM_CMD=<upstream-cmd.sh> so the original statusline runs above our line.

install.sh auto-builds dist/index.js if it's missing (the marketplace install only copies source, not the bundle). Re-running the slash command is always a no-op once installed.

If you want to preview what install will do, run /tokenplan-usage-hud:install --dry-run first.

If your active settings.json doesn't exist at the project level, install creates a minimal one (with permissions.defaultMode: bypassPermissions). It does not copy from the user-level file.

Restore from backup

/tokenplan-usage-hud:install --restore

Replaces the active settings.json with the most recent settings.json.bak.<ts>. Useful if you want to roll back an edit that wasn't made by us.

Commands

Four slash commands ship with the plugin:

Command What it does
/tokenplan-usage-hud:install Wire the wrapper into settings.json (or --uninstall / --restore).
/tokenplan-usage-hud:uninstall Restore settings.json, wipe cache + marketplace + loader rows.
/tokenplan-usage-hud:clean Trim old .bak.<ts> files (keeps the most recent per file).
/tokenplan-usage-hud:clean-cache Remove stale version dirs from the plugin cache, keeping only the newest.

Each is a Pattern B2 slash command — the body is a !-fenced shell block that loads scripts/<name>.sh directly via ${CLAUDE_PLUGIN_ROOT}, with allowed-tools scoped to that script. See Project layout for the file map.

Uninstall

/tokenplan-usage-hud:uninstall

This is a self-contained cleanup that works even after the plugin's cache and marketplace have been wiped. It does all of the following:

  1. Restore statusLine — strategy in order:
    • If ${CLAUDE_ROOT}/plugins/tokenplan-usage-hud/state/upstream-cmd.txt exists (the stable state dir, sibling of config.json), restore the original command byte-for-byte from that file.
    • Else, fall back to the highest-version cache dir's legacy state/upstream-cmd.txt (v0.2.18 and older layout, before the stable state dir existed). Same ordering as the wrapper's ls + sort + tail resolver.
    • Else, fall back to the most recent settings.json.bak.<ts> whose statusLine does not have _tokenplan_managed: true (the state before the plugin was installed).
    • Else, strip the marker but leave the wrapper in place and print a warning.
  2. Remove tokenplan-usage-hud@tokenplan-usage-hud from settings.json.enabledPlugins (other plugins preserved).
  3. Remove tokenplan-usage-hud from settings.json.extraKnownMarketplaces (Claude Code records the marketplace source there too — leaving it would re-add the marketplace on next /plugin marketplace add with no visible diff).
  4. Wipe cache/tokenplan-usage-hud/, marketplaces/tokenplan-usage-hud/, and the legacy marketplaces/cwf818-tokenplan-usage-hud/ alias.
  5. Strip the plugin's row from installed_plugins.json and known_marketplaces.json (with timestamped .bak.<TS> backups).
  6. Trim old .bak.<ts> files — invokes scripts/clean.sh as the final step so uninstall leaves a tidy filesystem (one newest backup per file). User-named backups like settings.json.bak-pre-v0.1.8 are NOT touched.

settings.json and the two JSON files are backed up before any destructive change. Line endings (CRLF/LF) are preserved. The script is idempotent — re-running on a clean system prints nothing to do and exits 0. Add --dry-run to preview actions without modifying anything.

The env block of settings.json (including your ANTHROPIC_AUTH_TOKEN) is not touched. The script runs locally with no API calls and never reads ANTHROPIC_AUTH_TOKEN.

After uninstall, re-install with the four-step flow:

/plugin marketplace add cwf818/tokenplan-usage-hud
/plugin install tokenplan-usage-hud@tokenplan-usage-hud
/reload-plugins
/tokenplan-usage-hud:install

The legacy /tokenplan-usage-hud:install --uninstall flag still works (it's a thin shim that calls the same uninstaller). Prefer the dedicated :uninstall slash command in new scripts.

For dev iteration, npm run dev:uninstall (or npm run dev:uninstall:dry) does the same thing from the command line.

Clean

/tokenplan-usage-hud:clean

Removes the old .bak.YYYYMMDDTHHMMSS backup files our installer leaves behind, keeping only the most recent one per base file:

  • settings.json.bak.<ts> → keeps the newest
  • installed_plugins.json.bak.<ts> → keeps the newest
  • known_marketplaces.json.bak.<ts> → keeps the newest

User-named backups (e.g. settings.json.bak-pre-v0.1.8) are not touched — only the script-generated timestamp pattern. Idempotent: if at most one backup exists per file, prints nothing to clean and exits 0. Add --dry-run to preview.

The uninstall slash command already runs clean.sh as its final step, so explicit cleanup is usually unnecessary after a fresh uninstall. Use the clean command directly if you want to tidy up between installs, or if you've accumulated a lot of .bak.<ts> files from earlier dev iteration.

For dev iteration, npm run settings:clean (or npm run settings:clean:dry) does the same thing from the command line.

Clean cache

/tokenplan-usage-hud:clean-cache

Every /plugin install rolls the cache forward — Claude Code creates a new <version> directory under <cache>/tokenplan-usage-hud/ but does not remove the previous one. Old version dirs pile up over time (~40-50 MB each: full source tree + node_modules). The statusLine.command written by :install is already version-independent — it ls -ds every version dir, sorts by version, and execs the highest — so old dirs are pure dead weight.

/tokenplan-usage-hud:clean-cache walks the cache, finds all ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ version directories, sorts numerically (so 0.2.10 sorts AFTER 0.2.9, not lexically), keeps the newest, and removes the rest.

Safety: non-version entries (.in_use, .orphaned_at_*, hidden dirs, files, anything not matching the version regex) are left untouched. Idempotent: re-running is a no-op once only the newest remains. Add --dry-run to preview.

How it composes with other statuslines

  • The wrapper script is scripts/wrapper.sh. If TOKENPLAN_UPSTREAM_CMD is set, it runs that path as a bash script (bash "$TOKENPLAN_UPSTREAM_CMD"), captures stdout, and exposes it to the plugin entry as the TOKENPLAN_UPSTREAM env var. If unset, the wrapper runs the plugin as the sole statusline.
  • TOKENPLAN_UPSTREAM_CMD is an absolute path to a bash script — install.sh writes one at ${CLAUDE_ROOT}/plugins/tokenplan-usage-hud/state/upstream-cmd.sh whose body is exec bash -c '<original-command>'. This path is stable (sibling of config.json, NOT inside the per-version cache dir), so /plugin install rolls don't move it. Older v0.1.10–v0.1.11 used bash -c against the path itself, which silently failed — fixed in v0.1.12.
  • The plugin preserves interior newlines in upstream output and injects \x1b[0m before its own line if upstream ends with an unclosed ANSI SGR — so multi-line, ANSI-colored upstream statuslines render correctly.

Activation

The plugin picks a provider from ANTHROPIC_BASE_URL and renders exactly one line:

ANTHROPIC_BASE_URL Line API
https://api.minimaxi.com/anthropic Usage: … / Remain: … GET https://www.minimaxi.com/v1/token_plan/remains
https://api.deepseek.com/anthropic Balance: … GET https://api.deepseek.com/user/balance
anything else (vanilla Anthropic, etc.) (hidden)

Both endpoints are called with Authorization: Bearer $ANTHROPIC_AUTH_TOKEN — the same token, no new env vars. The provider table lives in the providers config block; the defaults reproduce the v0.2.20 behavior (exact match against the /anthropic base URL). Other URL forms can be matched via COMPARE_METHOD: "INCLUDE" (substring) or "STARTWITH" (prefix with suffix-attack guard, so https://api.deepseek.com.evil.example is rejected). On vanilla Anthropic, OpenRouter, or any other provider the plugin doesn't recognize, the line is hidden and any upstream output passes through unchanged.

MiniMax token-plan line

 Usage: ▓▓▓▓▓░░░ 38% (47m🕖 5h) · ▓▓▓░░░░░ 39% (4d47m🕓 7d)
Remain: ░░░░░▓▓ 62% (47m🕖 5h) · ░░░▓▓▓▓ 61% (4d47m🕓 7d)

Two windows (5-hour + weekly), split-bar with colored percentage, reset countdown in parentheses, window label after the countdown. The bar glyphs flip in remaining mode — both modes read left-to-right as "what's spent → what's left":

  • used mode: ▓▓▓▓▓░░░ is consumed (colored), is remaining (plain)
  • remaining mode: ░░░░░▓▓ is consumed (plain), is remaining (colored)

The reset countdown uses the shared time-formatting template:

Remaining Rendered Note
-1ms 0m past-due, explicit "this window has reset"
30s <1m sub-minUnit floor
5m 5m
60m 1h0m internal zero preserved
90m 1h30m
24h 1d0h

maxUnitCount (default 2) controls how many units are shown. Leading zeros are dropped (0d0h5m5m); internal and trailing zeros are kept (2h0m2h0m, NOT 2h). See stale.maxUnitCount in the config schema for the full set of options.

DeepSeek balance line

When ANTHROPIC_BASE_URL matches the configured providers.deepseek entry (default: exact match against https://api.deepseek.com/anthropic), the plugin fetches the user's account balance and renders:

Balance: ¥110.00             # is_available=true, single CNY entry
Balance: $25.00               # is_available=true, single USD entry
Balance: ¥110 · $3.5         # multi-currency: ALL entries from balance_infos,
                             # joined by ' · ', single color band from the
                             # LOWEST balance (most urgent currency drives hue).
Balance: not available!       # is_available=false or no parseable entries

Per-currency display prefix: USD$, CNY / RMB. Any other currency code is rendered as itself, uppercased (e.g. EUR42.50).

5-band color thresholds on the lowest entry's numeric value:

Range Color
<5 red
[5,10) orange
[10,20) yellow
[20,50) dark green
>=50 bright green

Display mode

Default mode is used — the line begins with Usage: and the percentage shown is the percentage of the window you've consumed. The colored bar segment represents the consumed portion.

Switch to remaining mode via the config file:

{ "display": "remaining" }

See Configuration below for the full schema. The earlier TOKENPLAN_DISPLAY env var is gone as of v0.2.0 — anyone who used it must move the value to config.json.

In remaining mode the line begins with Remain: and the percentage is what's left; the colored bar segment represents the remaining portion.

display is MiniMax-only — DeepSeek's Balance: line doesn't have a percentage to flip.

Configuration

A single JSON file parameterizes every hardcoded tunable (color thresholds, cache TTL, fetch timeout, currency prefixes, bar geometry, stale-annotation formatting, display-mode label). Path:

  • Unix: ~/.claude/plugins/tokenplan-usage-hud/config.json
  • Windows: %USERPROFILE%\.claude\plugins\tokenplan-usage-hud\config.json

Loaded once at startup. Missing file → all defaults (today's behavior, bit-for-bit identical). Malformed JSON or a single bad field → one stderr line (tokenplan-usage-hud: config <reason>; using defaults) and the default for that field only — the rest of your config is still honored. The plugin never blanks the statusline on bad config.

A reference with every field is at config.example.json. Copy it to the path above and edit.

Schema (v1)

{
  "cacheTtlMs": 60000, // > 0; success-cache TTL in ms
  "fetchTimeoutMs": 5000, // > 0; per-request HTTP timeout
  "display": "used", // "used" | "remaining"
  "modeLabels": {
    // line prefix per mode
    "used": "Usage:",        // MiniMax plan line
    "remaining": "Remain:",  // MiniMax plan line in remaining mode
    "balance": "Balance:",   // DeepSeek balance line
  },
  "colors": {
    // 256-color ANSI palette
    "brightGreen": "brightGreen",
    "darkGreen": "darkGreen",
    "yellow": "yellow",
    "orange": "orange",
    "red": "red",
    "stale": "brightBlack", // dim-gray for the "⛓️‍💥 X ago" suffix
  },
  "thresholds": {
    // band cutoffs (4 ascending numbers each)
    "minimaxPercent": [20, 40, 60, 80],
    "deepseekBalance": [5, 10, 20, 50],
  },
  "currency": {
    // DeepSeek per-currency rendering
    "prefixes": { "USD": "$", "CNY": "", "RMB": "" },
    "fallback": "", // prefix for unknown currency codes
    "default": "CNY", // assumed currency when API omits one
  },
  "stale": {
    // stale-on-error annotation. v0.2.17 dropped the legacy
    // `separator` field — the stale annotation is now appended
    // directly after the template output. If a custom separator
    // is needed before the annotation, place it explicitly in the
    // lineTemplate (e.g. add an `s_0` token after `m_window7d`).
    "ageEmoji": { "healthy": "🔗", "broken": "⛓️‍💥" },
  },
  "cacheHitColors": {
    // v0.4.0+ — 3-band palette for the m_cacheHitRate module.
    // Bands chosen by cacheHitThresholds (in tokenFormat below).
    "good": "brightGreen", // ≥ 80%
    "warn": "yellow",      // ≥ 50%
    "bad": "orange",       // < 50%
  },
  "bar": {
    // bar geometry
    "width": 8, // 3..64
    "filled": "",
    "empty": "",
  },
  "timeFormat": {
    // Top-level knobs — govern ALL time rendering in the plugin
    // (reset countdown AND stale-age suffix). Keeping them out of the
    // `stale` block means a user who wants second-level granularity
    // anywhere gets it everywhere consistently.
    //
    // Smallest unit shown on time countdowns.
    //   "m" (default): sub-minute shows as "<1m" — the "<" prefix
    //                  signals "less than 1 minute" so the user can
    //                  tell a window is about to reset (vs "0m" which
    //                  would imply a definite wait).
    //   "s":           sub-minute shows as actual seconds (e.g. "47s").
    "minUnit": "m",
    // How many non-zero units to show. Drops LEADING zero units first,
    // then takes up to maxUnitCount from the start — including any
    // internal/trailing zero units. Clamped to [1, 4].
    //   1d2h3m4s → "1d2h"
    //   2h3m4s   → "2h3m"
    //   2h0m     → "2h0m"   (NOT "2h" — internal zeros preserved)
    //   0d0h5m   → "5m"     (leading zeros dropped)
    "maxUnitCount": 2,
  },
  "countdown": {
    // Reset-countdown visualization. Belongs with the countdown, NOT with
    // the stale-on-error annotation (which is a separate concern).
    //
    // Glyphs appended to the reset countdown (e.g. "2h3m🕛"). The picker
    // indexes by `remainingMs / resetDurationMs`, so the array reads
    // left-to-right as "few remaining → many remaining" (i.e. ascending
    // by remaining-time ratio). Index 0 is shown when remaining ≈ 0
    // (about to reset); the last entry is shown when remaining ≈ total
    // (fresh). Twelve clock-face emoji give a smooth visual ramp from
    // 12 o'clock (🕛, least remaining) around to 1 o'clock (🕐, most
    // remaining); two glyphs give a binary hourglass (full/empty);
    // one glyph is static. Providers without start_time (DeepSeek,
    // legacy) fall back to index 0.
    "resetArrows": [
      "🕛",
      "🕚",
      "🕙",
      "🕘",
      "🕗",
      "🕖",
      "🕕",
      "🕔",
      "🕓",
      "🕒",
      "🕑",
      "🕐",
    ],
  },
  "separators": [
    // Separator strings referenced from `lineTemplate` as s_0, s_1, ….
    // s_0 — between adjacent modules within a group (default: " ")
    // s_1 — the inter-group glyph (default: "·", just the symbol — no
    //       surrounding spaces; the default plan template composes
    //       s_0 + s_1 + s_0 to produce the visual " · ")
    // Add more entries to reference them as s_2, s_3, … in your
    // lineTemplate.
    " ",
    "·",
  ],
  "tokenFormat": {
    // v0.4.0+ — compact number formatting for the m_token* modules.
    //   < thresholds[0] → raw integer ("342")
    //   < thresholds[1] → "<x.y>k"   ("12.3k")
    //   ≥ thresholds[1] → "<x.y>M"   ("1.2M")
    "thresholds": [1000, 1000000],
    // Decimal places for the k / M tier (0..4)
    "precision": 1,
    // Decimal places for m_tokenInSpeed / m_tokenOutSpeed (0..4)
    "speedPrecision": 1,
    // Decimal places for m_cacheHitRate percentage (0..4)
    "cachePctPrecision": 1,
    // 3-band cache-hit thresholds (ascending). < lo → bad (orange),
    // < hi → warn (yellow), ≥ hi → good (green).
    "cacheHitThresholds": [50, 80],
  },
  "lineTemplates": {
    // v0.4.0+ — registry of reusable template fragments. Each value
    // is a token array. Allowed tokens: any m_* module EXCEPT
    // m_template, plus s_* separators. Keys are user-chosen; the
    // renderer reads from this registry when it sees an
    // `m_template:<key>` token inside `statuslineTemplate`.
    // Default entries `plan` and `balance` point at the v0.3.x
    // defaults so existing internal lookups still resolve; they're
    // auto-merged with your custom keys (your keys win on
    // collision).
    //
    // Example: a shared `header` chunk used in both plan and
    // balance templates.
    "header": ["m_modeLabel", "s_0"]
  },
  "statuslineTemplate": "1line",  // or a raw array, e.g.:
  // ["m_template:header:mode:plan", "s_0", "m_window5h"],

  // v0.4.0+ replaces the v0.3.x `lineTemplate: { plan, balance }`
  // shape with the two fields above. See the "Upgrading to v0.4.0"
  // section below for the migration notes. The loader warns once
  // per config load and ignores the legacy field.
  "providers": {
    // v0.2.21: declarative provider registry. The plugin picks a
    // provider by matching ANTHROPIC_BASE_URL against each entry's
    // BASE_URL_COMPARED_TO using the entry's COMPARE_METHOD. The
    // first match wins; iteration order = insertion order. The TYPE
    // field ("TOKEN_PLAN" | "BALANCE") is the dispatcher — it picks
    // the fetcher, the lineTemplate key, and the fail-line label.
    //
    // Defaults reproduce the v0.2.20 hardcoded behavior bit-for-bit.
    // Adding a new provider is a config-only change; partial overrides
    // inherit missing fields from the default.
    "minimax": {
      "TYPE": "TOKEN_PLAN",
      "BASE_URL_COMPARED_TO": "https://api.minimaxi.com/anthropic",
      "COMPARE_METHOD": "EXACT",
      "ENDPOINT": "https://www.minimaxi.com/v1/token_plan/remains",
    },
    "deepseek": {
      "TYPE": "BALANCE",
      "BASE_URL_COMPARED_TO": "https://api.deepseek.com/anthropic",
      "COMPARE_METHOD": "EXACT",
      "ENDPOINT": "https://api.deepseek.com/user/balance",
    },
  },
  // Plugin version is loaded automatically at startup from
  // .claude-plugin/plugin.json and surfaced via the m_version
  // module. No config field — just add "m_version" to your
  // lineTemplate to render "v0.2.17"-style annotations.
}

Each colors.* value is either a symbolic shortcut (brightGreen, darkGreen, yellow, orange, red, brightBlack) or a literal ANSI SGR string matching ^\x1b\[[0-9;]*m$. Strings containing newlines are rejected (statusline-injection guard).

thresholds.* must be exactly 4 finite ascending numbers. bar.width must be in [3, 64]. Numeric fields must be finite and (where relevant) positive. separators entries must be single-line strings; an entry containing \n is dropped (the rest of the array is preserved). lineTemplate.<key> must be a non-empty array of strings.

Providers

The providers block is a Record<string, ProviderEntry>. Each entry declares:

  • TYPE"TOKEN_PLAN" (5h + 7d two-window line) or "BALANCE" (account-balance line). Drives the fetcher, the lineTemplate key, and the fail-line label.
  • BASE_URL_COMPARED_TO — the URL pattern to match ANTHROPIC_BASE_URL against.
  • COMPARE_METHOD — one of three modes, all case-insensitive:
    • "EXACT" (default) — baseUrl === pattern. Safest; rejects URLs that aren't exactly the configured value.
    • "INCLUDE"baseUrl.includes(pattern). Fuzzy host match; useful when ANTHROPIC_BASE_URL adds a path you don't care about.
    • "STARTWITH"baseUrl.startsWith(pattern) with a suffix-attack guard: the character right after the prefix must be undefined, /, ?, or #. This rejects https://api.deepseek.com.evil.example even though it startsWith("https://api.deepseek.com"). The deepseek matcher in earlier versions used this scheme; the v0.2.21 default is EXACT (a stricter choice), so users who relied on the old prefix behavior should set COMPARE_METHOD: "STARTWITH".
  • ENDPOINT — the provider's API URL. Must start with http:// or https://.

A user can override any subset of fields on a known provider; missing fields inherit from the default. To add a new provider, append a new key:

{
  "providers": {
    "moonshot": {
      "TYPE": "BALANCE",
      "BASE_URL_COMPARED_TO": "https://api.moonshot.cn/anthropic",
      "COMPARE_METHOD": "EXACT",
      "ENDPOINT": "https://api.moonshot.cn/v1/users/me/balance",
    },
  },
}

The cache key for a provider's response is its name (so two TOKEN_PLAN providers get separate cache slots). The matcher's iteration order = insertion order of the providers object — the first matching entry wins on a tie.

Module tokens

The line layout is declared as statuslineTemplate (v0.4.0+). It accepts a preset name (e.g. "1line", "standard", "abundant") OR a raw token array. The default is "1line" — the same shape v0.3.x rendered with its default lineTemplate.plan.

  • For shared / reusable fragments, register them under lineTemplates and pull them into statuslineTemplate with m_template:<key>[:mode:<plan|balance>]. See m_template below.
  • String-form statuslineTemplate only accepts the FIXED preset names (the renderer looks them up in the bundled PLAN_PRESETS / BALANCE_PRESETS tables; the table to search is chosen by the provider's TYPE — TOKEN_PLAN → PLAN_PRESETS, BALANCE → BALANCE_PRESETS). Arbitrary lineTemplates keys are NOT accepted as preset names; use the array form with m_template:<key> instead.

Three token shapes:

  • m_<name> — a display module, rendered in order. Modules that have no content in the current context (e.g. m_window7d when the weekly data is missing) emit nothing, AND their immediately adjacent s_N tokens are skipped too — so a hidden window doesn't leave orphan separators in the output.
  • s_<n> — a separator reference, looked up in separators[n]. Out-of-range references expand to "" and trigger a one-time stderr warning.
  • Inline-args tokens (v0.3.3+) — m_<name>:<args>, s_<n>:<args> — see the Inline-args grammar below for the full syntax. Lets you emit arbitrary literal text (m_label:<string>) and tint separators (s_<n>:color:<c>) without registering a new module.

Recognized modules:

Token Renders
m_modeLabel The leading prefix: modeLabels.used (plan) or modeLabels.balance (DeepSeek)
m_window5h 5-hour bar + colored percentage (e.g. ▓▓▓░░░ 38%)
m_countdown5h 5-hour reset suffix: (2h3m🕛 5h) when reset time known, or just 5h otherwise
m_window7d 7-day bar + colored percentage
m_countdown7d 7-day reset suffix: (4d16h🕛 7d) or just 7d
m_balance The DeepSeek balance chunk (e.g. $25 · ¥110), single SGR-wrapped block
m_age The age annotation: 🔗 5m ago (fresh, in-template) or ⛓️‍💥 5m ago (stale, in-template or forced fallback). Emits unconditionally when listed in the lineTemplate; returns null only when ageMs is missing.
m_version The plugin version: v0.3.3 (auto-loaded from .claude-plugin/plugin.json)
m_tokenIn Per-API-call input tokens — e.g. in:140. v0.4.0+ semantics: shows delta(current_usage.input_tokens) when delta(cost.total_api_duration_ms) > 0. Otherwise (first tick, no API call, session change, regression) renders in:0 — "0" reads as "tracking but nothing new this tick" rather than the more ambiguous --. A valid zero-delta API call (deltaIn == 0 with deltaApi > 0) also renders in:0. For session-cumulative, use m_tokenInTotal.
m_tokenOut Per-API-call output tokens — out:265 (or out:0 for all missing-data / zero-delta cases). Same semantics as m_tokenIn.
m_tokenTotal / m_tokenSession Session cumulative total (input + output + cache) — e.g. tot:163k / session:163k. Two names for the same metric; pick whichever reads better in your template.
m_ctx Current post-turn context length (excludes output) — e.g. ctx:163k. Reads current_usage.{input + cache_creation + cache_read}.
m_cacheHitRate Cache hit rate as a percentage with 3-band coloring (good ≥ 80%, warn ≥ 50%, bad < 50%) — e.g. green cache:99%. Reads current_usage.{cache_read, cache_creation}.
m_cacheRead Cache read tokens + context share — e.g. dim-gray cache:163k (99.2%). Hidden when cache traffic is zero.
m_token5h Tokens used in the last 5h (delta between first and last sample in the window) — e.g. 5h:12k. Reads state/<projectHash>/<sessionId>.jsonl (v0.4.x+ Per-Project Layout; was state/token-samples/<hash>/<sid>.jsonl in v0.4.0–v0.4.). Hidden when fewer than 2 samples exist for the window.
m_token7d Same, for the last 7 days.
m_tokenInSpeed Per-API-call input speed — e.g. dim-gray in:32.4 t/s. v0.4.0+: math is delta(current_usage.input_tokens) / delta(cost.total_api_duration_ms) * 1000. Always renders — every missing-data case (first tick, no-API-call, session change, regression) AND the zero-token-delta case collapse to in:-- t/s rather than 0.0 t/s. One consistent "no throughput to report" signal across all branches.
m_tokenOutSpeed Same for output tokens.
m_tokenInAvg Per-session running-average input speed — e.g. dim-gray in:18.2 t/s. v0.4.0+: math is sum(delta_in) / sum(delta_api) * 1000, accumulator stored under tickAvg:<sessionId> in the per-project state/<projectHash>/cache.json (separate from the per-tick tickSpeed: snapshot). Only valid-API-call ticks contribute (delta_api > 0 AND delta_in >= 0); idle and regression ticks don't accumulate. Always renders — in:-- when no valid tick has accumulated yet (sumApi is 0).
m_tokenOutAvg Same for output tokens.
m_totalTokenIn Per-session running total of input tokens across valid-API-call ticks — e.g. in:340. v0.4.0+: reads the same tickAvg:<sessionId> cache slot as m_tokenInAvg's numerator; AvgSnapshot is extended with sumCache to accommodate the cache-read module below. Only delta_api > 0 ticks contribute; idle and regression ticks don't accumulate. Always renders — in:0 when no valid tick has accumulated yet.
m_totalTokenOut Same for output tokens — e.g. out:265.
m_totalTokenWithCacheIn Per-session running total of cache_read_input_tokens across valid-API-call ticks — e.g. cache:490k. Renders cache:-- when stdin lacks current_usage.cache_read_input_tokens (honest "data unavailable" signal); cache:0 when no valid tick has accumulated yet.
m_quote An inspirational short quote from a 100+ entry bilingual pool (English + 中文). See the m_quote section below for the :freq: and :color: parameters.

v0.4.0+ session-info / metadata modules (read the live stdin payload captured by /statusline):

Token Renders
m_session The session name — e.g. strip-diagnostics-display. Reads stdin session_name.
m_model The model display name — e.g. MiniMax-M3. Reads stdin model.display_name.
m_effort The effort level — e.g. high. Reads stdin effort (accepts string or {level} object).
m_repo Repository identity — e.g. github.com/cwf818/tokenplan-usage-hud. Reads stdin workspace.repo.{host, owner, name}, drops null components.
m_ccVersion The Claude Code CLI version — e.g. 2.1.191. Reads stdin version.
m_sessionDuration Elapsed session time — e.g. 20h42m. Reads stdin cost.total_duration_ms in 1d2h3m format.
m_sessionApiDuration API-call time within the session — e.g. 2h18m. Reads stdin cost.total_api_duration_ms.
m_linesAdded Session-cumulative lines added — e.g. + 3965 (with leading space). Reads stdin cost.total_lines_added.
m_linesRemoved Session-cumulative lines removed — e.g. - 967. Reads stdin cost.total_lines_removed.
m_tokenInTotal Session-cumulative input tokens — e.g. in:163k. v0.4.0: new module, replaces the pre-v0.4.0 m_tokenIn semantic. Reads stdin context_window.total_input_tokens.
m_tokenOutTotal Session-cumulative output tokens — e.g. out:155. v0.4.0: new module, replaces the pre-v0.4.0 m_tokenOut semantic.
m_contextSize Context window size (compact) — e.g. 200.0k. Reads stdin context_window.context_window_size.
m_contextUsed Context used percentage — e.g. 63%. Reads stdin context_window.used_percentage.
m_windowContext Context bar + 5-band-colored percentage, parallel to m_window5h / m_window7d — e.g. ▓▓▓▓▓░░░ 63%. Synthesized from used_percentage.
m_template:<key> (v0.4.0+) Expand a lineTemplates[<key>] fragment into the current render. See m_template below.

Visibility of m_age (priority: template-driven, stale fallback):

  • If your lineTemplate includes m_age, the module emits unconditionally (no stale gating). Emoji reflects the fetch state: 🔗 X ago on fresh ticks (showing the cache age), ⛓️‍💥 X ago on stale (showing time since last successful fetch). Hidden only when ageMs is missing.
  • If your lineTemplate does NOT include m_age, the stale fallback kicks in: when the fetch result is stale (network failure with a cached value), the broken-chain annotation is appended to the rendered line. On fresh ticks, no annotation is shown — the broken-chain indicator is reserved for real outages. The dedup check looks for any " ago" tail on the rendered lines, so a user who does include m_age in their template gets exactly one annotation, not two.

Inline-args grammar (v0.3.3+)

Token forms that take colon-delimited parameters:

Token form Required params Optional params Description
m_label:<string> string (literal text) color Emit <string> verbatim, optionally wrapped in <color> SGR.
m_modeLabel[:color:<c>] (string from ctx) color Same as today's bare m_modeLabel, optionally tinted. The string is derived from modeLabels.used/remaining/balance based on ctx.mode and ctx.balance.
s_<n>[:color:<c>] index color The separator at index n (from separators[]), optionally tinted.
m_<name>[:color:<c>] (v0.3.4+, any module) color Tint the natural output of any other module. See Per-module :color: override below.

The grammar after the prefix is <param1>:<value1>[:<param2>:<value2>…]. The first segment is the value of the implicit first parameter (string for m_label, index for s_<n>); subsequent segments come in name:value pairs. Both halves are validated against the per-prefix schema (INLINE_SCHEMAS in src/render.ts); any malformed token is dropped with a one-shot stderr warn.

<color> accepts either a shortcut name (brightGreen, darkGreen, yellow, orange, red, stale, brightBlack) or a raw SGR string (\x1b[36m). Anything else triggers the same one-shot warn.

The bare forms (m_modeLabel, s_0, m_window5h, m_tokenIn, …) keep working exactly as today — the inline-args path only fires when the token contains :. So upgrading to v0.3.4 does NOT change the default lineTemplate output. Examples (opt-in — add to your lineTemplate to enable):

{
  "lineTemplate": {
    "plan": [
      "m_modeLabel:color:brightGreen",  // tint the leading Usage: prefix
      "s_0",                            // plain space (no color)
      "m_window5h", "s_0", "m_countdown5h",
      "s_0:color:darkGreen",            // tint the middle separator
      "s_1", "s_0",
      "m_window7d", "s_0", "m_countdown7d"
    ],
    "balance": [
      "m_label:$:color:yellow",         // emit "$" in yellow, then space, then balance
      "s_0",
      "m_balance"
    ]
  }
}

Per-module :color: override (v0.3.4+)

Every existing module — m_window5h, m_window7d, m_countdown5h, m_countdown7d, m_balance, m_age, m_version, m_tokenIn, m_tokenOut, m_tokenTotal, m_tokenSession, m_ctx, m_cacheHitRate, m_cacheRead, m_token5h, m_token7d, m_tokenInSpeed, m_tokenOutSpeed, m_tokenInAvg, m_tokenOutAvg, m_totalTokenIn, m_totalTokenOut, m_totalTokenWithCacheIn, plus the v0.4.0+ session-info modules (m_session, m_model, m_effort, m_repo, m_ccVersion, m_sessionDuration, m_sessionApiDuration, m_linesAdded, m_linesRemoved, m_tokenInTotal, m_tokenOutTotal, m_contextSize, m_contextUsed, m_windowContext) — also accepts an optional :color:<c> segment. Two cases:

  • Plain-text modules (e.g. m_version, m_tokenIn, m_countdown5h, m_ctx): the override simply wraps the natural output in <color>…<RESET> SGR. The module's own body is unchanged.
  • Already-colored modules (e.g. m_window5h, m_balance, m_cacheHitRate, m_cacheRead, m_age, m_tokenInSpeed, m_tokenOutSpeed): the override replaces the natural color choice — band-based, cache-hit-band, or fixed stale color — with your <color>. The user's color always wins; if you didn't say :color:, the module keeps its existing coloring and the default lineTemplate output is byte-for-byte identical to v0.3.3.

Conflict rule: if a :color: is supplied, the natural color is ignored (per your spec — "如果与现有颜色方案冲突,则无视该参数" — the override always wins when present).

Examples:

{
  "lineTemplate": {
    "plan": [
      "m_modeLabel:color:brightGreen",   // tint the leading prefix
      "s_0",
      "m_window5h:color:red",            // force the 5h bar/percent to red
      "s_0", "m_countdown5h",
      "s_0", "s_1", "s_0",
      "m_window7d", "s_0", "m_countdown7d",
      "s_0", "m_age:color:yellow",       // tint the stale annotation
      "s_0", "m_tokenIn:color:darkGreen" // tint the session input-token chunk
    ],
    "balance": ["m_modeLabel", "s_0", "m_balance:color:red"]
  }
}

The bare forms (m_window5h, m_age, m_tokenIn, …) still go through the original MODULES path, so users on the default template see no diff on upgrade.

Extension point: future parameterized modules (m_model:..., …) plug in by adding an entry to INLINE_SCHEMAS and INLINE_RENDERERS in src/render.ts. No new top-level config keys needed.

Per-module :display: override (v0.4.0+, window modules only)

The three window modules — m_window5h, m_window7d, m_windowContext — accept an optional :display:used or :display:remaining segment. This is the per-module counterpart to the top-level display config field: it overrides which side of the bar gets colored and which percentage is shown, but only for the one module that uses it. The global config is untouched.

Token What it does
m_window5h:display:used 5h bar in used mode (same as bare when display=used in config)
m_window5h:display:remaining 5h bar in remaining mode (inverts percentage; uses the remaining-mode palette)
m_window7d:display:used / :remaining Same, for the 7d window
m_windowContext:display:used / :remaining Same, for the context window

The bare forms are byte-for-byte unchanged — the global display config (default used) still drives them. Combine with :color: for both axes:

{
  "lineTemplate": {
    "plan": [
      "m_modeLabel", "s_0",
      "m_window5h:display:remaining:color:yellow", "s_0", "m_countdown5h",
      "s_0", "s_1", "s_0",
      "m_window7d:display:remaining:color:yellow", "s_0", "m_countdown7d"
    ],
    "balance": ["m_modeLabel", "s_0", "m_balance"]
  }
}

Valid values are exactly used or remaining (case-sensitive). display:USED, display: (empty), or any other value is a parse-fail — the token is dropped and the standard one-shot "unknown lineTemplate module" warn fires.

Note: the remaining-mode palette is the reverse of the used-mode palette: high remaining = healthy = brightGreen, low remaining = red. So m_window5h:display:remaining at 38% used renders 62% in the band-3 remaining color (darkGreen) — not the band-3 used color (orange). See formatOneChunk / splitBar for the exact mapping.

m_template:<key>[:mode:<plan|balance>] (v0.4.0+)

Pulls a registered fragment from lineTemplates into the rendered template. Use it to share chunks (e.g. a Usage: / Balance: label, a separator) across plan and balance templates without duplicating tokens.

Token forms

Token form Required params Optional params Description
m_template:<key> key (the lineTemplates entry to expand) mode (default plan), nulldrop (accepted, no-op for this module) Expand the registered fragment into the current render.
m_template:<key>:mode:plan key nulldrop Same, but the chunk only renders when the provider's mode key is plan.
m_template:<key>:mode:balance key nulldrop Same, but only renders when the provider's mode key is balance.

Behavior:

  • Missing key → warns once and drops the chunk (same as any unknown module).
  • Mode mismatch → silently drops (no warn). The user explicitly asked for a mode filter, so no error is needed.
  • Nesting is impossible: the loader strips any m_template: tokens from lineTemplates entries at load time. A lineTemplates value cannot contain another m_template: token, so recursion cannot happen.
  • :color: is silently ignored on m_template: put :color: on the inner modules if you want per-module coloring. Color propagation across expanded templates was deferred (the cost/complexity didn't justify the feature).

Example — share a label across both providers:

{
  "lineTemplates": {
    "header": ["m_modeLabel", "s_0"]
  },
  "statuslineTemplate": [
    "m_template:header:mode:plan",  // visible only on plan providers (TOKEN_PLAN)
    "m_window5h", "s_0", "m_countdown5h",
    "s_2",
    "m_tokenIn"
  ]
  // On a DeepSeek provider the header chunk drops (mode:plan ≠ balance)
  // and the renderer falls through to the default balance rendering
  // (BALANCE_PRESETS["simple"] = ["m_modeLabel", "s_0", "m_balance"]).
}

Upgrading to v0.4.0 from v0.3.x

The lineTemplate: { plan, balance } config field is removed in v0.4.0. The loader emits one tokenplan-usage-hud: config lineTemplate is removed in v0.4.0; use lineTemplates + statuslineTemplate. See CHANGELOG.md for the upgrade path. Ignoring the legacy field. warning per config load and ignores the legacy field — there is no auto-promotion of lineTemplate.planlineTemplates.plan.

To migrate a customized lineTemplate:

- "lineTemplate": {
-   "plan":   ["m_modeLabel", "s_0", "m_window5h", "s_0", "m_countdown7d"],
-   "balance": ["m_modeLabel", "s_0", "m_balance"]
- }
+ "lineTemplates": {
+   // Optional — only needed if you want to share fragments.
+   // The renderer reads `statuslineTemplate` first; if you don't
+   // need shared chunks, just set `statuslineTemplate` below and
+   // skip this block.
+ },
+ "statuslineTemplate": [
+   "m_modeLabel", "s_0",
+   "m_window5h", "s_0", "m_window7d"
+ ]

The default statuslineTemplate is "1line", which reproduces the v0.3.6 default rendering — only customized configs require manual migration. To switch presets, set "statuslineTemplate": "standard" / "abundant" / "complete" (the full list lives in PLAN_PRESETS / BALANCE_PRESETS at the top of src/config.ts).

m_quote (v0.3.6+)

An inspirational short quote, drawn from a 100+ entry bilingual pool (English + 中文). Opt-in — the default lineTemplate does NOT include it; add it where you want it.

Token forms

Form Default freq Default color Description
m_quote h none Plain quote, no SGR wrap.
m_quote:freq:<numeric-time> (the one supplied) none Pick how often the quote rotates (see below).
m_quote:color:<c> h (the one supplied) Tint the quote (see below).
m_quote:freq:<…>:color:<…> Combine both.

Frequency (freq) — single-unit time format

The freq argument is a <digits><unit> string (bare unit letter = 1<unit>). Multi-unit forms like 2h10m are rejected — express 130 minutes as 130m. The unit letter picks the bucket size; the digit prefix picks the count.

freq Window size Boundary anchor
s 1s Unix-epoch multiples (since 1s divides 1d, also UTC-aligned)
m 1m UTC midnight-aligned (1m divides 1d)
h 1h UTC midnight-aligned (default)
d 24h UTC midnight-aligned
2h 2h UTC midnight-aligned (divides 24h)
12h 12h UTC midnight-aligned (00:00 / 12:00 UTC)
6h 6h UTC midnight-aligned (00:00 / 06:00 / 12:00 / 18:00 UTC)
30m 30m UTC midnight-aligned
7d 7d Rolling (7d does not divide 1d, so boundaries are epoch-relative)
13h 13h Rolling
130m 130m Rolling

Anchor rule: when the bucket size divides one day (86_400_000 % bucket === 0), the boundary sits on UTC midnight. Otherwise the boundary sits at Unix-epoch multiples of the bucket. This gives predictable wall-clock times for "round" windows like 12h / 6h / 30m, and accepts arbitrary windows like 13h or 7d for users who want them.

Two ticks within the same window always produce the same quote. Multi-unit forms (2h10m), leading zeros (01h), zero counts (0h), oversize counts (> 1_000_000), unknown units (5x), and malformed inputs (+5h, 1.5h, h10) are all rejected — the token is dropped with a one-shot stderr warn.

Color (color)

Accepts the standard 7 shortcuts (brightGreen, darkGreen, yellow, orange, red, stale, brightBlack), any raw SGR string (\x1b[36m), and three special values unique to m_quote:

Color value Effect
rainbow Per-character 256-color SGR using a 6-hue palette (cyan → blue → purple → magenta → orange → yellow). Rotates through the palette for each character of the quote.
rand-rainbow Same as rainbow, but the palette rotation starts at a different offset. Two adjacent freq windows with the same quote but different rand-rainbow renders will look distinct.
hue Single-hue SGR wrap for the whole quote. The hue is picked from the 6×6×6 256-color cube using a hash of the quote text, so each quote gets a deterministic but varied color.

Rainbow / rand-rainbow / hue colors are also stable within a freq window — same window, same colors — so a tick-by-tick refresh of the statusline never visually strobes.

Example template

{
  "lineTemplate": {
    "plan": [
      "m_modeLabel", "s_0",
      "m_window5h", "s_0", "m_countdown5h",
      "s_0", "s_1", "s_0",
      "m_window7d", "s_0", "m_countdown7d",
      "s_2",                              // newline separator (see "Module tokens")
      "m_quote:freq:12h:color:rainbow"   // twice-daily rotating rainbow quote
    ],
    "balance": ["m_modeLabel", "s_0", "m_balance", "s_2", "m_quote:color:hue"]
  }
}

Behavior notes

  • The pool has 110+ entries; the renderer is deterministic per (freq, nowMs) so the same window always shows the same quote. No Math.random / no Date.now inside the renderer.
  • An invalid freq value (e.g. m_quote:freq:yearly, m_quote:freq:2h10m) drops the token with a one-shot stderr warn.
  • An invalid color value drops the token with a one-shot stderr warn.

Recipes

Lower cache TTL (re-fetch more often):

{ "cacheTtlMs": 15000 }

Switch to remaining mode:

{ "display": "remaining" }

Custom palette (e.g. cyan-only):

{
  "colors": {
    "brightGreen": "\x1b[38;5;51m",
    "darkGreen": "\x1b[38;5;45m",
    "yellow": "\x1b[38;5;81m",
    "orange": "\x1b[38;5;75m",
    "red": "\x1b[38;5;69m"
  }
}

Token usage (v0.4.0+)

In addition to the tokenplan 5h/7d window percentages, the plugin reads Claude Code's session JSON from stdin and exposes a suite of opt-in m_token* modules. The default lineTemplate does NOT include any token module — existing v0.3.x configs render byte-identical after upgrade. To opt in, add the desired modules to your lineTemplate.plan (and/or balance).

Available data sources (parsed once per tick from stdin, zero IO):

  • context_window.total_input_tokens / total_output_tokens — session cumulative
  • context_window.current_usage.{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens} — post-turn snapshot
  • cost.total_duration_ms — session wall-clock duration (used by speed modules)
  • session_id, cwd, transcript_path — used to scope the state file for 5h/7d modules

Persistent state file (only for m_token5h / m_token7d): one JSON line per tick, appended to ~/.claude/plugins/tokenplan-usage-hud/state/<projectHash>/<sessionId>.jsonl (v0.4.x+ Per-Project Layout; was state/token-samples/<hash>/<sid>.jsonl in v0.4.0–v0.4.). ~120B per row, ~700KB over 7d. Lives in the stable state/ directory — survives cache rolls and version bumps. Legacy state/token-samples/<hash>/<sid>.jsonl files can be preserved across an upgrade with bash scripts/migrate-state.sh (preview with --dry-run).

Example template with token counts alongside the windows:

{
  "lineTemplate": {
    "plan": [
      "m_modeLabel", "s_0",
      "m_window5h", "s_0", "m_countdown5h",
      "s_0", "s_1", "s_0",
      "m_window7d", "s_0", "m_countdown7d",
      "s_0", "s_1", "s_0",
      "m_tokenIn", "s_0", "m_tokenOut", "s_0", "m_ctx",
      "s_0", "m_cacheHitRate",
    ],
  }
}

Renders (example): Usage: ▓░░░░░░░ 9% (4h47m🕔 5h) · ▓▓░░░░░░ 25% (2d8h🕔 7d) · in:163.5k out:155 ctx:163.5k cache:100.0%

Token-format config (tokenFormat block):

{
  "tokenFormat": {
    // Compact notation thresholds: < thresholds[0] → raw integer,
    // < thresholds[1] → "12.3k", else → "1.2M".
    "thresholds": [1000, 1000000],
    // Decimal places for the k/M tier (0..4)
    "precision": 1,
    // Decimal places for m_tokenInSpeed / m_tokenOutSpeed (0..4)
    "speedPrecision": 1,
    // Decimal places for m_cacheHitRate percentage (0..4)
    "cachePctPrecision": 1,
    // 3-band cache-hit thresholds (ascending). < lo → bad,
    // < hi → warn, ≥ hi → good.
    "cacheHitThresholds": [50, 80],
  },
  "cacheHitColors": {
    "good": "brightGreen",
    "warn": "yellow",
    "bad": "orange",
  },
}

Module reference: see the Module tokens table above for the full m_token* list with output examples. The full design rationale (data source choice, slice strategy, color policy, state file shape) lives at memory/token-usage-design-adr.md in the source repo.

Show only the 5-hour window (drop the 7-day window):

{
  "lineTemplate": {
    "plan": ["m_modeLabel", "s_0", "m_window5h", "s_0", "m_countdown5h"],
    "balance": ["m_modeLabel", "s_0", "m_balance"]
  }
}

Custom inter-window separator (e.g. / instead of ·):

{
  "separators": [" ", " / "],
  "lineTemplate": {
    "plan": [
      "m_modeLabel", "s_0",
      "m_window5h", "s_0", "m_countdown5h",
      "s_0", "s_1", "s_0",
      "m_window7d", "s_0", "m_countdown7d"
    ],
    "balance": ["m_modeLabel", "s_0", "m_balance"]
  }
}

Show the plugin version at the end of the line:

{
  "lineTemplate": {
    "plan": [
      "m_modeLabel", "s_0",
      "m_window5h", "s_0", "m_countdown5h",
      "s_0", "s_1", "s_0",
      "m_window7d", "s_0", "m_countdown7d",
      "s_0", "m_version"
    ],
    "balance": ["m_modeLabel", "s_0", "m_balance", "s_0", "m_version"]
  }
}

Migration from TOKENPLAN_DISPLAY

If you previously set TOKENPLAN_DISPLAY=remaining in your shell, move that value into config.json:

mkdir -p ~/.claude/plugins/tokenplan-usage-hud
echo '{ "display": "remaining" }' > ~/.claude/plugins/tokenplan-usage-hud/config.json

Restart Claude Code (or run /reload-plugins) for the change to take effect.

Diagnostics log

When the plugin encounters something worth telling you about — a malformed config field, a fetcher that returned an unexpected status code — it can append a JSONL entry to:

~/.claude/plugins/tokenplan-usage-hud/state/<projectHash>/diagnostics.jsonl

(v0.4.x+ Per-Project Layout: the log is partitioned by project directory so multiple Claude Code sessions in different projects never contend over the same file. The legacy top-level state/diagnostics.jsonl is still written for plugin-level errors with no project affiliation — e.g. config-parse warnings emitted before any cwd is known.)

Each line is a structured record:

{"at":1782576199672,"level":"warning","source":"config","msg":"invalid 'bar.width' value (got abc); using default 8"}
{"at":1782576200103,"level":"error","source":"api","msg":"MiniMax /v1/token_plan/remains returned non-zero base_resp.status_code (status_code=1008)"}

Use it as a postmortem trail — tail -f while debugging, or grep by level and source when something went wrong yesterday. JSONL is greppable and structured (timestamp + level + source + message).

Opt-in gate

The log is OFF by default — set TOKENPLAN_DIAGNOSTICS_ENABLE=1 (or true / yes, case-insensitive) in your shell to enable file writes:

export TOKENPLAN_DIAGNOSTICS_ENABLE=1

The rationale: the file lives in your plugins dir and may contain sensitive fragments (config paths, error text from upstream libraries). We don't write unless you explicitly ask. The stderr noise for append failures stays independent of the gate — silent when the write succeeds, present when it doesn't.

Size policy

Capped at the last 200 entries (~40KB). Anything older than 200 events is uninteresting by definition — we just want a tail. Trim is best-effort and runs after every append.

Wiping the log

/tokenplan-usage-hud:clean --purge-runtime walks every state/<projectHash>/ subdir and wipes its diagnostics.jsonl, cache.json, and <*.jsonl> token-sample files (v0.4.x+ Per-Project Layout). It also cleans the legacy top-level state/diagnostics.jsonl, state/cache.json, and the legacy state/token-samples/ tree for users upgrading from v0.4.0–v0.4.. Top-level upstream-cmd.{sh,txt} and config.json are NEVER purged. Preview first with /tokenplan-usage-hud:clean --purge-runtime --dry-run.

Auth

The plugin reuses process.env.ANTHROPIC_AUTH_TOKEN to call the provider's plan endpoint. No new env vars. See SECURITY.md for how the token is handled.

Caching

The Claude Code statusline is updated in response to interaction events by default (every prompt, every tool result). Starting with Claude Code 2.1.97, the statusLine.refreshInterval field is honored, letting the statusline refresh on a fixed cadence instead. Two scopes of "refresh interval" are involved and they're independent:

  • This plugin's 60 s TTL — how long we cache a successful API response before re-fetching. MiniMax and DeepSeek have different rate-limit policies and refresh cadences; 60 s is a deliberate default that keeps the statusline responsive without hammering the API. Cache entries are shadowed to disk under state/<projectHash>/cache.json (sibling of config.json, wiped by :uninstall; v0.4.x+ Per-Project Layout — was state/cache.json in v0.4.0–v0.4.), so the TTL is honored across per-tick child-process spawns — the second tick within 60 s reuses the first tick's value instead of re-fetching. Per-project isolation: render.ts prefixes every cache key with <projectHash>: so different projects never collide on the same cache.json.

  • Claude Code's statusLine.refreshInterval — how often the harness invokes the statusline command. Set in ~/.claude/settings.json independently of this plugin:

    {
      "statusLine": {
        "type": "command",
        "command": "...",
        "refreshInterval": 60
      }
    }

    Unit is seconds (not milliseconds — that's a frequent footgun; the harness will reject or misbehave on values like 30000). For this plugin, 30–120 s is a sensible range: shorter than the 60 s TTL is wasted, much longer and the line lags behind reality. The refreshInterval value does not affect the API-call TTL — they are independent knobs.

    This plugin follows the minimum-change principle: it does not write refreshInterval into settings.json. Set it yourself if you want a non-default cadence.

DeepSeek balance uses a separate cache key ("balance") so the two providers don't invalidate each other. (v0.2.21: the cache key is actually the provider's name in the providers map — "minimax" / "deepseek" by default. Adding a third provider gets its own slot automatically.)

Failure handling

Three outcomes when the provider API is called:

Outcome What you see on the statusline
Fresh fetch The normal Usage: … / Balance: … line, no suffix on the default template. (Includes within-TTL cache hits — the broken-chain suffix is reserved for stale state. If your template includes m_age, you'll see a healthy 🔗 X ago here instead.)
Fetch failed, cache exists The last good value, with a dim ⛓️‍💥 X ago suffix at the end (e.g. Balance: ¥110 ⛓️‍💥 5m ago). The broken-chain emoji IS the indicator (no leading separator).
Fetch failed, no cache Usage: not available! (MiniMax) or Balance: not available! (DeepSeek) in red. Plugin is alive but the provider is unreachable.

The X ago format uses the same template as the reset countdown: d/h/m units, maxUnitCount=2 default, drop leading zeros but keep internal/trailing zeros. Sub-minute follows timeFormat.minUnit: minUnit="m"<1m ago; minUnit="s"${seconds}s ago. Examples (with default minUnit="m"):

Cached age Rendered suffix
30 s ⛓️‍💥 <1m ago
5 min ⛓️‍💥 5m ago
90 min ⛓️‍💥 1h30m ago
24 h ⛓️‍💥 1d0h ago

The hard-fail not available! line intentionally has no age suffix because there is no cached value to be stale-OF.

Develop

npm install
npm run typecheck    # tsc --noEmit
npm test             # node --test --import tsx src/**/*.test.ts
npm run build        # esbuild → dist/index.js
npm run dev          # esbuild --watch

Response shape

The MiniMax parser is defensive and tries multiple plausible field names:

Window Keys tried (in order)
5-hour five_hour, fiveHour, fivehour, 5h, hour5
Weekly weekly, week, wk, seven_day, sevenDay, 7d

Inside each window: remaining / left / available; limit / total / quota; used (used ⇒ remaining = limit − used). Both data.{…} envelope and flat shapes are accepted. If base_resp.status_code ≠ 0, the response is treated as failure and the line is omitted.

The verified real shape (captured 2026-06-24 against https://www.minimaxi.com/v1/token_plan/remains):

{
  "model_remains": [
    {
      "model_name": "...",
      "current_interval_remaining_percent": 60,
      "current_weekly_remaining_percent": 92,
      "end_time": "...",
      "weekly_end_time": "..."
    }
  ],
  "base_resp": { "status_code": 0 }
}

The plugin picks the entry with the lowest interval remaining % as the source of truth (the most-active model). If you capture a fresh response and the shape diverges, save it as src/__fixtures__/remains.real.json and tighten the parser in src/api.ts.

The DeepSeek response shape is simpler — { is_available: bool, balance_infos: [{ currency, total_balance, granted_balance, topped_up_balance }, ...] } — and the parser iterates all entries so every currency the account holds is rendered.

Dev loop: re-installing the plugin from scratch

When iterating on the install flow (changes to scripts/install.sh, scripts/uninstall.sh, the slash commands, the version, etc.) you need to fully wipe the plugin's on-disk state before /plugin install will re-fetch a clean copy. The plugin loader caches marketplace state and refuses to bump an already-installed plugin — on Windows this surfaces as EPERM: operation not permitted, rename ... -> ... .bak.

Use the bundled dev helper (does not touch settings.json — your statusLine is preserved):

# Preview what will be removed:
npm run dev:uninstall:dry

# Wipe tokenplan-usage-hud state:
npm run dev:uninstall

It removes:

  • the tokenplan row from installed_plugins.json and known_marketplaces.json (with timestamped .bak.<ts> backups of both files)
  • cache/tokenplan-usage-hud/, marketplaces/tokenplan-usage-hud/, and the loader's leftover marketplaces/cwf818-tokenplan-usage-hud/ directory

Then re-install:

/plugin marketplace add cwf818/tokenplan-usage-hud
/plugin install tokenplan-usage-hud@tokenplan-usage-hud
/reload-plugins
/tokenplan-usage-hud:install

If the loader still says "EPERM" after dev:uninstall, the most common cause is a Claude Code process holding a file lock on the marketplace dir. Quit all running Claude Code sessions (not just this one) and re-run npm run dev:uninstall.

Project layout

src/
  index.ts            # entry — stdin drain, provider dispatch, cache, render, compose
  types.ts            # Provider = string | null; ProviderType / CompareMethod / ProviderEntry
  providers.ts        # URL matching, fetcher / template / fail-label dispatch (v0.2.21)
  api.ts              # MiniMax fetch + tolerant parser for /v1/token_plan/remains
  api.deepseek.ts     # DeepSeek fetch + parser for /user/balance
  render.ts           # pure: pctBar + ANSI color thresholds + formatLine + formatBalanceLine
  cache.ts            # 60s TTL + stale-on-error; getWithAge returns cache age on within-TTL hit
  composition.ts      # reads TOKENPLAN_UPSTREAM, prepends (preserving ANSI/multi-line) and appends line
  diagnostics.ts      # JSONL append logger (opt-in via TOKENPLAN_DIAGNOSTICS_ENABLE)
  token-store.ts      # append-only JSONL state file for m_token5h / m_token7d (v0.4.0+)
  __fixtures__/       # remains.real.json, balance.real.json, balance.multi.json, …
  *.test.ts           # node:test unit tests
.claude-plugin/
  plugin.json         # plugin manifest (declares commands)
  marketplace.json    # single-plugin marketplace wiring
commands/
  install.md          # /tokenplan-usage-hud:install slash command
  uninstall.md        # /tokenplan-usage-hud:uninstall slash command
  clean.md            # /tokenplan-usage-hud:clean slash command
scripts/
  wrapper.sh          # bash wrapper: TOKENPLAN_UPSTREAM_CMD → TOKENPLAN_UPSTREAM → us
  install.sh          # settings.json patcher (install + thin shim for --uninstall)
  uninstall.sh        # self-contained uninstaller (used by :uninstall and dev:uninstall)
  clean.sh            # trim old .bak.<ts> files; --purge-runtime also wipes state/<projectHash>/{cache.json,diagnostics.jsonl,*.jsonl} + legacy top-level + token-samples
  lib/edit-settings.mjs  # ESM helper used by install.sh
  dev-uninstall.sh    # DEV-ONLY thin shim → exec uninstall.sh
settings.example.json # template (NEVER commit real settings.json)

License

MIT — see LICENSE.

About

A provider-agnostic Claude Code statusline plugin for token-plan usage / remaining quota.

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages