Warning
This project is archived. Live token / cache metering is now natively supported by the GitHub Copilot CLI (built-in footer, statusLine.command, and quota display), so this plugin is no longer maintained. The repository remains available for reference.
Live token meter for GitHub Copilot CLI. Shows input / output / cache tokens and agent activity for every prompt — in your terminal title bar, in a live dashboard, and via a
/tokensslash command.
| Surface | How |
|---|---|
| Always-visible "footer" | The plugin hooks sessionStart, userPromptSubmitted, postToolUse, and agentStop and writes a one-line summary to your terminal title bar via OSC 2. Works in iTerm2, Kitty, Alacritty, WezTerm, Terminal.app, gnome-terminal, Windows Terminal. |
| Copilot CLI custom status line | A copilot-tokens statusline subcommand renders a colourised one-liner that drops straight into Copilot CLI 1.0.52+'s statusLine.command slot (the custom row in /footer → Configure Status Line). Reads the Copilot stdin payload + the plugin's cached state, so the row stays in sync with the rest of the footer. |
/tokens slash command |
Inside Copilot CLI, type /tokens for a multi-line breakdown — per-model, per-turn, with quota. |
copilot-tokens CLI |
Companion CLI with status, summary, watch, top, json, recompute, statusline, statusline-command subcommands for use in a second pane / tmux / CI. |
| Status JSON for other tools | ~/.copilot/state/token-meter/latest.json is rewritten on every hook tick — point your tmux status bar, polybar, iTerm status, or VS Code status item at it. |
Copilot CLI's built-in footer (settings.json → footer) only supports a fixed set of toggles (showModelEffort, showDirectory, showBranch, showContextWindow, showQuota, …). The newer custom row (Copilot CLI 1.0.52+) lets you point at a statusLine.command, but stops there: it doesn't aggregate, repair, or attribute tokens for you.
This plugin layers two surfaces on top of that:
- An OSC 2 terminal-title-bar writer driven by lifecycle hooks (always visible above the prompt, no Copilot CLI version required).
- A
copilot-tokens statuslinesubcommand built to be thestatusLine.commandtarget — it does the aggregation, the input-tokens-zero repair, and the per-tool attribution itself, and renders a compact, colourised one-liner.
Both surfaces share the same data pipeline, which piggy-backs on four stable contracts the CLI already exposes:
- Plugin lifecycle hooks (
sessionStart,userPromptSubmitted,postToolUse,agentStop). events.jsonlin~/.copilot/session-state/<sessionId>/— typed againstschemas/session-events.schema.json— for turn / tool / message / model activity and theassistant.message.outputTokensfield.- Process telemetry log at
~/.copilot/logs/process-*.log— the CLI writes one debug log per process containing[Telemetry] cli.telemetry:JSON blocks. Eachkind: "assistant_usage"block carries the full token / cache breakdown (input_tokens,output_tokens,cache_read_tokens,cache_write_tokens,reasoning_tokens,duration,ttft_ms) and is joined back to the originating turn viaprovider_call_id↔assistant.message.requestId. Eachkind: "copilot_user_info"block carries a live quota snapshot. - Raw HTTP response bodies logged in the same process log under
[DEBUG] data: { ... "usage": { "prompt_tokens", "completion_tokens", "prompt_tokens_details": { "cached_tokens", "cache_creation_tokens" } } }. These are matched to telemetry records byid↔api_call_idand used to repair theinput_tokens: 0that current Copilot CLI builds always report when prompt caching is in play (the truthful uncached input isprompt_tokens - cached_tokens - cache_creation_tokens).
The hook script merges the two sources, aggregates totals, and writes an OSC 2 title-bar escape to /dev/tty so you get a true always-visible footer above your shell prompt.
Telemetry-log parsing is incremental: per-
(session, log)byte offsets are cached in~/.copilot/state/token-meter/telemetry-cache.json, so only the new tail of each log is read on every hook tick. Event IDs are deduped so the same usage event isn't billed twice when multiple processes touch the same session.
The plugin is a self-contained directory you can install into Copilot CLI with copilot plugin install.
# From the GitHub marketplace (recommended):
copilot plugin install abhi-singhs/copilot-token-meter
# Or from a local checkout (handy while developing the plugin itself):
copilot plugin install /path/to/copilot-token-meterThe marketplace install snapshots the plugin into Copilot CLI's plugin cache and
registers it in ~/.copilot/config.json. Local installs additionally record the
source path so copilot plugin update <name> can re-snapshot after edits.
After editing files in a local checkout, refresh the snapshot:
copilot plugin update copilot-token-meterTo uninstall:
copilot plugin uninstall copilot-token-meterImportant: Plugins are loaded once per
copilotsession. Restart the CLI after install / update for hooks to take effect.
Add the bin/ directory to your $PATH (optional — only needed to call
copilot-tokens directly from your shell):
export PATH="/path/to/copilot-token-meter/bin:$PATH"The plugin uses only Node's built-in modules — no npm install needed for runtime use. (npm test is supported for the test suite.)
In ~/.copilot/settings.json:
{
"footer": {
"showContextWindow": true,
"showQuota": true
}
}These are built-in items that give you complementary signal (context-window % full and remaining premium-request quota) alongside the token-meter's title-bar.
> /tokens
The skill runs copilot-tokens summary and replies with the formatted breakdown.
copilot-tokens # one-line summary
copilot-tokens summary # detailed multi-line breakdown
copilot-tokens watch # live dashboard, refreshes every 1s
copilot-tokens top # top sessions by total tokens, all time
copilot-tokens top --limit 5 # only show the top 5
copilot-tokens top --since 7d # only sessions active in the last 7 days
copilot-tokens json # raw status JSON (for scripting)
copilot-tokens recompute # force re-scan of the current session
copilot-tokens statusline # render Copilot CLI's custom status line
# (consumes Copilot payload JSON on stdin)
copilot-tokens statusline-command # print the path string to paste into
# ~/.copilot/config.json statusLine.command
copilot-tokens --plain summary # no ANSI colour (also honours NO_COLOR=1)Drop-in status-bar configs for tmux, polybar, iTerm2, and WezTerm live in examples/.
set -g status-right '#(cat ~/.copilot/state/token-meter/latest.json 2>/dev/null | jq -r .title) | %H:%M'See examples/tmux.conf for a fuller version that also surfaces context-window utilisation.
Copilot CLI 1.0.52+ ships a built-in Configure Status Line menu (open it with /footer) that includes a custom row sourced from a statusLine.command you configure. When enabled, Copilot spawns the command on every state change, pipes its session JSON to it on stdin, and renders the command's stdout as a footer row beneath the built-in items.
The plugin exposes a copilot-tokens statusline subcommand purpose-built for this contract. It reads Copilot's stdin payload, joins it with the plugin's cached telemetry, and prints a single ANSI-coloured line like:
↑12.3k ↓45.6k ⟳88.0k ⊕5.1k 🧠1.2k · 📦42% · 7t/23🔧
To wire it up:
-
Get the absolute command path:
copilot-tokens statusline-command # → "/Users/you/.copilot/installed-plugins/_direct/abhi-singhs--copilot-token-meter/bin/copilot-tokens statusline" -
Add it to
~/.copilot/config.json:{ "statusLine": { "type": "command", "command": "/absolute/path/to/copilot-tokens statusline", "padding": 0 } }(See
examples/copilot-statusline.mdfor a ready-to-merge snippet and full walkthrough.) -
Open the Copilot CLI configurator with
/footer, scroll tocustom, and pressenterto toggle it on. Save withesc.
The command is fail-safe: any error (missing cache, malformed payload, unreadable state file) is swallowed and the row renders empty rather than breaking Copilot's UI. Honours NO_COLOR=1 and --plain / --no-color for terminals that don't render ANSI in the footer.
Why both this and the terminal title bar? The title bar is always visible above your shell prompt and works in any terminal that honours OSC 2; the custom status line lives inside Copilot CLI's footer so it sits next to
directory,branch,effort,context-used, andquota. Pick whichever surface fits your workflow — running both is fine and incurs no extra cost.
Read ~/.copilot/state/token-meter/latest.json and render the .title field. See examples/iterm2-wezterm.md for the exact iTerm2 Script status-bar component and a WezTerm update-right-status Lua snippet.
See examples/polybar.ini for a custom/script module that mirrors the title bar.
copilot[opus-4.7-1m] ↑12.3k ↓4.2k ⟳88.0k ⊕5.1k 🧠1.2k 7t/23🔧
| Symbol | Meaning |
|---|---|
↑ |
input tokens (new prompt tokens sent to the model) |
↓ |
output tokens (model's reply tokens) |
⟳ |
cache read tokens (reused prefix from prompt cache) |
⊕ |
cache write tokens (new prefix written to prompt cache) |
🧠 |
reasoning tokens (shown only when > 0; OpenAI reasoning models, etc.) |
Nt/M🔧 |
turns / tool calls |
The summary view additionally shows:
- Context-window utilization as a bar + percentage, computed against the most recent API call's prompt size (cache_read + cache_write + input) so it reflects what the model actually saw.
- Burn rate — tokens/min over the last 5 / 30 minutes and an ETA to context-window exhaustion at the current rate (when telemetry is producing fresh data).
- Per-tool attribution —
By toolsection showing which tools (bash,view,edit, …) burned the most input/cache tokens, with proportional1/Nsplitting across batched tool calls. - Sub-agent rollup — when sub-agents (the
tasktool) make their own API calls, theSub-agentssection surfaces their token usage separately, while still folding it into the parenttasktool entry. - Prompt-tokens (= input + cache_read + cache_write — what actually crossed the wire), total billed tokens (Anthropic-style formula), cumulative API duration, and a live
Quotablock sourced fromcopilot_user_infotelemetry events.
| Path | Purpose |
|---|---|
~/.copilot/state/token-meter/latest.json |
Snapshot of the most recently updated session. |
~/.copilot/state/token-meter/<sessionId>.json |
Per-session snapshot. |
~/.copilot/state/token-meter/history-<sessionId>.jsonl |
Rolling burn-rate history (capped). |
~/.copilot/state/token-meter/current |
Plain-text file containing the current session ID. |
~/.copilot/state/token-meter/telemetry-cache.json |
Per-(session, log) byte offsets + de-duped event_id set + apiResponse cache for incremental telemetry parsing. |
~/.copilot/state/token-meter/telemetry-cache.lock |
Transient advisory lockfile (held only briefly during cache writes). |
~/.copilot/state/token-meter/models.json |
Optional user override for per-model context-window sizes. |
~/.copilot/state/token-meter/hooks.log |
One-line trace per hook invocation (handy for debugging that hooks fire at all). |
~/.copilot/state/token-meter/errors.log |
Hook error log (rotated by logrotate etc.). |
| Env var | Effect |
|---|---|
COPILOT_HOME |
Override the ~/.copilot location (used in tests / CI). |
COPILOT_TOKENMETER_RESET_ON_STOP=1 |
Clear the terminal title bar when the agent stops, instead of leaving the final stats lingering. |
NO_TITLE=1 / TERM=dumb |
Suppress the OSC 2 title-bar writes entirely. |
NO_COLOR=1 |
Disable ANSI colour in copilot-tokens output (per no-color.org). Equivalent to passing --plain / --no-color. |
Drop a models.json at ~/.copilot/state/token-meter/models.json to add or override context-window sizes for new or custom models. Entries with match prefixes earlier in the list win; longest match wins among matches with the same name.
{
"models": [
{ "match": ["my-byok-model"], "contextWindow": 200000 }
]
}After editing models.json, delete ~/.copilot/state/token-meter/telemetry-cache.json so the next hook tick re-resolves model metadata.
- Check
~/.copilot/state/token-meter/hooks.log— each hook invocation appends a single line. If it's empty, the hooks aren't firing. - Restart Copilot CLI after
copilot plugin installorcopilot plugin update. Hooks are loaded once per session. - Confirm your terminal honours OSC 2 (
printf '\e]2;hello\a'should set the title). The plugin no-ops onTERM=dumband whenNO_TITLE=1is set. - On macOS, Terminal.app's "Tab" title and "Window" title are separate; OSC 2 sets the window title.
- If you're running inside
tmux, tmux owns the outer title — useset -g set-titles onandset -g set-titles-string '#{pane_title}'to pass the pane title through.
- Telemetry logs lag the first few prompts. Run a couple of
/helpor a quick prompt, then re-check. - Confirm process logs exist and are recent:
ls -lt ~/.copilot/logs/process-*.log | head. The plugin only reads logs newer than 14 days. - Force a re-scan:
copilot-tokens recompute.
This is the known input_tokens: 0 bug in current Copilot CLI builds when prompt caching is active. The plugin repairs it by joining each telemetry record to the matching raw [DEBUG] data: { ... usage: { prompt_tokens, prompt_tokens_details } } block via api_call_id. The repair only kicks in after the raw response is logged — usually one tick later. If it persists across multiple turns, your CLI build may be omitting the [DEBUG] data: blocks; check that ~/.copilot/logs/process-*.log actually contains them.
Pricing/context-window data is read lazily and cached for the lifetime of each hook process. Edit-then-restart-CLI is enough. If you're running copilot-tokens from a long-running watcher, kill and restart it. As a stronger reset, also delete ~/.copilot/state/token-meter/telemetry-cache.json.
Check ~/.copilot/state/token-meter/errors.log. The hook script is wrapped so any error is logged there and the process exits 0 — a hook failure never blocks Copilot CLI. If you see repeated errors, please file an issue with the relevant log excerpt and your Copilot CLI version.
The hooks are meant for Copilot CLI only. As of 0.2.2, every hook in hooks.json gates on $COPILOT_PLUGIN_ROOT (set by Copilot CLI 1.0.56+) and immediately exits without doing anything when that variable is unset — so the meter never fires under VS Code's Copilot chat agent or any other host. This also fixes the older MODULE_NOT_FOUND crash, where a non-CLI host ran the hook with cwd = workspace folder and Node couldn't resolve ./bin/token-meter.js. If you're on an older hooks.json, run copilot plugin update copilot-token-meter (or re-install) to pick up the new gating.
Pass --plain (or --no-color), or set NO_COLOR=1 in the environment. The context-window bar falls back to a plain-ASCII [## ] indicator.
- Output tokens & activity (turns, tool calls, message counts, model) come from
events.jsonl, which is the same record the CLI's own UI reads from. These are always present. - Input / cache_read / cache_write / reasoning / duration come from
[Telemetry] cli.telemetry:assistant_usageblocks in~/.copilot/logs/process-*.log. The plugin joins each usage block to the originating turn viaprovider_call_id↔assistant.message.requestId. These match exactly what the CLI reports to its quota service. - Input-tokens repair: current Copilot CLI builds always emit
input_tokens: 0when prompt caching is active. The truthful value is derived from the matching raw HTTP response body ([DEBUG] data: { id, usage:{ prompt_tokens, prompt_tokens_details:{...} } }) usingprompt_tokens − cached_tokens − cache_creation_tokens, joined bydata.id== telemetryapi_call_id. Without this repair theinputcolumn reads 0 even when there's real uncached content. - Per-tool attribution is approximate: each batched tool call gets a
1/Nshare of the next API call's prompt-fold tokens (input + cache_read + cache_write). For single-tool turns the attribution is exact; for batches of N tools it splits evenly. - Sub-agent attribution: sub-agent telemetry records carry
initiator: "sub-agent"andparent_tool_call_id. We attribute their tokens to the parent tool (typicallytask) and also surface a separate Sub-agents section. - Reasoning tokens are surfaced separately when the provider reports them (OpenAI o-series / gpt-5 reasoning models). Anthropic does not split reasoning out from output tokens, so
🧠typically reads 0 on Claude models — the reasoning is already inside↓. - Billed tokens (in
summaryview) is computed asinput + output + cache_write + cache_read/10, matching Anthropic's published billing formula. If you're on GitHub-managed routing or a BYOK provider, the actual billing rule may differ — treat this as an upper-bound estimate. - Quota (premium interactions / chat / completions remaining) is pulled from
copilot_user_infotelemetry blocks emitted by the CLI itself, so it tracks/usagedirectly. - If telemetry logs are unavailable (very fresh session, logs rotated away), the meter degrades gracefully to events-only mode —
outputTokensand activity are still accurate, the rest read 0, and thesummarybanner switches toSource: events.jsonl only.
For authoritative billing and quota use
/usageinside Copilot CLI — this plugin does not estimate USD cost.
# Run the unit tests
npm test # → node --test test/
# Sanity-check syntax of every script
node --check bin/token-meter.js bin/copilot-tokens lib/*.jsCI runs the same checks across Node 18/20/22 on Ubuntu / macOS / Windows via .github/workflows/test.yml.
The codebase is intentionally dependency-free — only Node built-ins are used at runtime and at test time. Source layout:
lib/
format.js number / duration / title formatting helpers
io.js JSONL reader, atomic file writes, OSC 2 title writer
pricing.js model → context-window lookup (with user override)
telemetry.js process-log parsing (telemetry + raw API responses)
aggregate.js events.jsonl + telemetry → totals / per-model / per-tool
bin/
token-meter.js plugin hook script (writes status JSON + title bar)
copilot-tokens companion CLI (status / summary / watch / top / json)
MIT