Context, observed tokens and account usage in one compact footer.

A native Claude Code mod that displays Context, Tokens and Usage together on one compact line below the terminal prompt. All interface text is in English.
From your terminal, install directly from the hosted marketplace:
claude plugin install aikya-usage --marketplace jlema0590/aikya-mods
This installs only aikya-usage. To install the other mods, run their matching commands in the installation guide.
From the repository root:
bash mods/aikya-usage/install.sh
Or in Claude Code, from the public repository:
/plugin install aikya-usage --marketplace jlema0590/aikya-mods
Restart Claude Code. The footer is enabled automatically; no usage card occupies the side pane. Flow and BoxBot remain independent.
When claude runs in an IDE's integrated terminal, this line belongs to Claude's terminal prompt and continues to use Claude's own metrics. See the integrated-terminal validation matrix for configuration sharing and actual per-IDE validation status.
Example values:
Context: 42% | Tokens: 53k | Usage 5h [====------] 40% | 7d [========--] 80%
N/A; they are never derived from token counts.Each window has its own bar: = is used capacity and - is remaining capacity. Its percentage follows the bar. A ten-cell bar is approximate; the percentage retains the native reading rounded to at most one decimal. A value above 100% fills the bar without hiding the actual percentage.
Consumed cells and their percentage use the theme's success color below 70%, warning from 70% to below 90%, and error at 90% or higher. In a typical dark theme these are green, yellow and red. The windows are colored independently. Brackets and labels stay neutral, remaining cells are dim, and N/A uses question-mark cells with an inactive color. The thresholds are visual cues, not account limits or task progress.
Bars shorten from ten to five, two or one cell as space decreases, and labels are abbreviated in narrow terminals. A numeric fallback keeps the window labels and readings when bars cannot fit. The host can still clip the smallest format if the terminal is extremely narrow or other native footer content occupies the row. No additional row is added. Colors follow the user's theme and terminal color preferences; the mod does not force ANSI colors. The detailed command preserves the full meaning of each value. The permanent footer uses the native PromptHint site where the host exposes it; the terminal is the target surface. Other interfaces can consult /aikya-usage for the same data.
Once Usage 0.1.1 is available in the marketplace, update an existing installation and restart Claude Code:
claude plugin marketplace update aikya-mods
claude plugin update aikya-usage@aikya-mods
/aikya-usage
/aikya-usage hide
/aikya-usage show
The default command reports the current context, token categories, coverage and native estimated USD cost without opening a pane or calling a model. Missing counts and prices show N/A; zero is used only for an actual zero reading.
The token ledger deduplicates request identities by agent, turn and step. A final turn total replaces the observed step subtotal, so it is not added twice. The ledger resets after clear/resume/session end and reload. It retains up to 256 turns and 256 steps per turn; hitting a bound marks coverage partial and does not evict old identities to recount them.
The mod reads the inexpensive native usage snapshot and observes existing streams. It does not request full context token counts, inspect credentials, write files, change permissions or make model/network requests. Cost is Claude Code's estimate, not an invoice.
claude plugin validate mods/aikya-usage --strict
claude plugin test mods/aikya-usage
GENERATED BY AIKYA HARNESS
hooks/register.js 76 lines1import { normalizeUsage, createLedger, recordStep, recordTurn, readLedger, footerParts, details, turnKey } from '../lib/usage.js';
2
3async function refresh($, state) {
4 const epoch = state.epoch;
5 try { const raw = await $.session.usage(); if (epoch === state.epoch) state.raw = raw; }
6 catch { if (epoch === state.epoch) state.raw = {}; }
7 $.ui.invalidate('ui.render');
8}
9
10export function register(on) {
11 const state = { raw: {}, ledger: createLedger(), epoch: 0, closed: false, turns: new Set(), visible: true };
12 on('session.start', async ($, e, next) => {
13 await refresh($, state);
14 await $.command.register({ name: 'aikya-usage', description: 'Show context, observed token totals and account usage', argumentHint: '[show|hide]', immediate: true });
15 return next(e);
16 });
17 on('command.run', { command: 'aikya-usage' }, async ($, e) => {
18 const action = e.args.trim();
19 if (action === 'hide' || action === 'show') {
20 state.visible = action === 'show';
21 $.ui.invalidate('ui.render');
22 return {};
23 }
24 if (action) return { text: 'Usage: /aikya-usage [show|hide]' };
25 await refresh($, state);
26 return { text: details(normalizeUsage(state.raw, await $.clock.now()), readLedger(state.ledger)) };
27 });
28 on('session.measure', async ($, e, next) => {
29 state.raw = e;
30 $.ui.invalidate('ui.render');
31 return next(e);
32 });
33 on('turn.start', async ($, e, next) => {
34 state.closed = false;
35 state.turns.add(turnKey(e));
36 return next(e);
37 });
38 on('turn.step', async function* ($, e, next) {
39 const epoch = state.epoch;
40 state.turns.add(turnKey(e));
41 const result = yield* next(e);
42 if (epoch === state.epoch && !state.closed) {
43 recordStep(state.ledger, e, result.usage);
44 $.ui.invalidate('ui.render');
45 }
46 return result;
47 });
48 on('turn.complete', async ($, e, next) => {
49 if (!state.closed && state.turns.has(turnKey(e))) {
50 recordTurn(state.ledger, e);
51 state.turns.delete(turnKey(e));
52 $.ui.invalidate('ui.render');
53 }
54 return next(e);
55 });
56 on('session.end', async ($, e, next) => {
57 state.raw = {};
58 state.ledger = createLedger();
59 state.turns.clear();
60 state.epoch += 1;
61 state.closed = true;
62 $.ui.invalidate('ui.render');
63 return next(e);
64 });
65 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
66 if (!state.visible || (e.surface !== 'terminal' && e.surface !== 'desktop')) return next(e);
67 const { Text } = $.ui.resolve(e);
68 // PromptHint shares its row with Claude's mode indicators and separators.
69 const columns = Math.max(24, (e.viewport?.columns || 80) - 24);
70 const parts = footerParts(normalizeUsage(state.raw, await $.clock.now()), readLedger(state.ledger), columns);
71 return Text({ wrap: 'truncate-end', children: parts.map(segment => Text({
72 color: segment.tone, dimColor: segment.tone === 'text' || segment.tone === 'inactive', children: [segment.text],
73 })) });
74 });
75}
76lib/usage.js 121 lines1const FIELDS = ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens'];
2const finite = value => typeof value === 'number' && Number.isFinite(value) && value >= 0;
3const metric = value => finite(value) ? value : null;
4
5export function normalizeUsage(raw, nowMs) {
6 const data = raw && typeof raw === 'object' ? raw : {};
7 const context = data.context || {};
8 const quota = kind => {
9 const window = Array.isArray(data.rateLimits) ? data.rateLimits.find(row => row?.kind === kind) : undefined;
10 if (!window || !finite(window.percentUsed)) return null;
11 if (window.resetsAt !== undefined) {
12 const reset = Date.parse(window.resetsAt);
13 if (!Number.isFinite(reset) || reset <= nowMs) return null;
14 }
15 return window.percentUsed;
16 };
17 const percent = metric(context.percent);
18 return {
19 contextPercent: percent !== null && percent <= 100 ? percent : null,
20 contextTokens: metric(context.tokens), window: metric(context.window),
21 fiveHour: quota('five_hour'), sevenDay: quota('seven_day'), costUsd: metric(data.cost?.usd),
22 };
23}
24
25export function createLedger() { return { turns: new Map(), partial: false }; }
26export function turnKey(event) { return JSON.stringify([event.agentId || '', event.turnId]); }
27function counts(usage) {
28 if (!usage || !FIELDS.every(field => Number.isSafeInteger(usage[field]) && usage[field] >= 0)) return null;
29 return FIELDS.map(field => usage[field]);
30}
31function row(ledger, event) {
32 if (typeof event.turnId !== 'string' || !event.turnId) { ledger.partial = true; return null; }
33 const key = turnKey(event);
34 if (!ledger.turns.has(key)) {
35 if (ledger.turns.size >= 256) { ledger.partial = true; return null; }
36 ledger.turns.set(key, { steps: new Map(), final: null, missing: false });
37 }
38 return ledger.turns.get(key);
39}
40export function recordStep(ledger, event, usage) {
41 const turn = row(ledger, event);
42 if (!turn) return;
43 const value = counts(usage);
44 if (!value || !Number.isSafeInteger(event.index) || event.index < 0) { turn.missing = true; return; }
45 if (!turn.steps.has(event.index) && turn.steps.size >= 256) { turn.missing = true; return; }
46 turn.steps.set(event.index, value);
47}
48export function recordTurn(ledger, event) {
49 const turn = row(ledger, event);
50 if (!turn) return;
51 const value = counts(event.usage);
52 if (value) turn.final = value;
53 else turn.missing = true;
54}
55export function readLedger(ledger) {
56 const values = [0, 0, 0, 0];
57 let observed = false, partial = ledger.partial;
58 for (const turn of ledger.turns.values()) {
59 if (!turn.final && turn.missing) partial = true;
60 const rows = turn.final ? [turn.final] : [...turn.steps.values()];
61 for (const counts of rows) {
62 observed = true;
63 counts.forEach((value, index) => { values[index] += value; });
64 }
65 }
66 const total = values.reduce((sum, count) => sum + count, 0);
67 if (!Number.isSafeInteger(total)) return { total: null, partial: true };
68 return { input: observed ? values[0] : null, output: observed ? values[1] : null, cacheRead: observed ? values[2] : null, cacheWrite: observed ? values[3] : null, total: observed ? total : null, partial };
69}
70function number(value) {
71 if (value === null || value === undefined) return 'N/A';
72 if (value >= 1000000000) return (value / 1000000000).toFixed(1).replace(/\.0$/, '') + 'b';
73 if (value >= 1000000) return (value / 1000000).toFixed(1).replace(/\.0$/, '') + 'm';
74 if (value >= 1000) return (value / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
75 return String(value);
76}
77const percent = value => value === null ? 'N/A' : String(Math.round(value * 10) / 10) + '%';
78const part = (text, tone = 'text') => ({ text, tone });
79export function usageTone(value) {
80 return !finite(value) ? 'inactive' : value < 70 ? 'success' : value < 90 ? 'warning' : 'error';
81}
82function barParts(value, cells) {
83 const reading = metric(value), tone = usageTone(reading);
84 if (!cells) return [part(percent(reading), tone)];
85 const filled = reading === null ? 0 : Math.round(Math.min(reading, 100) * cells / 100);
86 return [part('['), part('='.repeat(filled), tone),
87 part((reading === null ? '?' : '-').repeat(cells - filled), 'inactive'), part('] '), part(percent(reading), tone)];
88}
89export function usageBar(value, cells) {
90 return barParts(value, cells).map(segment => segment.text).join('');
91}
92export function footerParts(view, totals, columns) {
93 const context = percent(view.contextPercent), tokens = number(totals.total);
94 for (const [cells, prefix, separator] of [
95 [10, `Context: ${context} | Tokens: ${tokens} | Usage 5h `, ' | 7d '],
96 [5, `Ctx:${context} Tok:${tokens} | 5h `, ' | 7d '],
97 [2, `C:${context} T:${tokens} 5h`, ' 7d'],
98 [1, `C:${context} T:${tokens} 5h`, ' 7d'],
99 [0, `C${context} T${tokens} 5h`, ' 7d'],
100 ]) {
101 const parts = [part(prefix), ...barParts(view.fiveHour, cells), part(separator), ...barParts(view.sevenDay, cells)];
102 if (parts.reduce((length, segment) => length + segment.text.length, 0) <= columns || !cells) return parts;
103 }
104}
105export function footer(view, totals, columns) {
106 return footerParts(view, totals, columns).map(segment => segment.text).join('');
107}
108export function details(view, totals) {
109 const reading = value => value === null || value === undefined ? 'N/A' : String(value);
110 return [
111 'AIKYA USAGE', footer(view, totals, 200),
112 `Context tokens/window: ${reading(view.contextTokens)}/${reading(view.window)}`,
113 'Observed tokens since this mod loaded (including cached input):',
114 `Input: ${reading(totals.input)} | Output: ${reading(totals.output)}`,
115 `Cache read: ${reading(totals.cacheRead)} | Cache write: ${reading(totals.cacheWrite)}`,
116 `Coverage: ${totals.partial ? 'Partial; some token records are missing' : 'Observed requests only; earlier history is not included'}`,
117 `Estimated session cost (USD): ${view.costUsd === null ? 'N/A' : view.costUsd.toFixed(4)}`,
118 'Usage percentages are account-window readings from Claude Code; they are not inferred from tokens.',
119 ].join('\n');
120}
121