Files
claude-hud/dist/stdin.js
2026-04-04 02:24:24 +00:00

204 lines
7.0 KiB
JavaScript

import { AUTOCOMPACT_BUFFER_PERCENT } from './constants.js';
export async function readStdin() {
if (process.stdin.isTTY) {
return null;
}
const chunks = [];
try {
process.stdin.setEncoding('utf8');
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const raw = chunks.join('');
if (!raw.trim()) {
return null;
}
return JSON.parse(raw);
}
catch {
return null;
}
}
export function getTotalTokens(stdin) {
const usage = stdin.context_window?.current_usage;
return ((usage?.input_tokens ?? 0) +
(usage?.cache_creation_input_tokens ?? 0) +
(usage?.cache_read_input_tokens ?? 0));
}
/**
* Get native percentage from Claude Code v2.1.6+ if available.
* Returns null if not available or invalid, triggering fallback to manual calculation.
*/
function getNativePercent(stdin) {
const nativePercent = stdin.context_window?.used_percentage;
if (typeof nativePercent === 'number' && !Number.isNaN(nativePercent)) {
return Math.min(100, Math.max(0, Math.round(nativePercent)));
}
return null;
}
export function getContextPercent(stdin) {
// Prefer native percentage (v2.1.6+) - accurate and matches /context
const native = getNativePercent(stdin);
if (native !== null) {
return native;
}
// Fallback: manual calculation without buffer
const size = stdin.context_window?.context_window_size;
if (!size || size <= 0) {
return 0;
}
const totalTokens = getTotalTokens(stdin);
return Math.min(100, Math.round((totalTokens / size) * 100));
}
export function getBufferedPercent(stdin) {
// Prefer native percentage (v2.1.6+) so the HUD matches Claude Code's
// own context output. The buffered fallback only approximates older versions.
const native = getNativePercent(stdin);
if (native !== null) {
return native;
}
// Fallback: manual calculation with buffer for older Claude Code versions
const size = stdin.context_window?.context_window_size;
if (!size || size <= 0) {
return 0;
}
const totalTokens = getTotalTokens(stdin);
// Scale buffer by raw usage: no buffer at ≤5% (e.g. after /clear),
// full buffer at ≥50%. Autocompact doesn't kick in at very low usage.
const rawRatio = totalTokens / size;
const LOW = 0.05;
const HIGH = 0.50;
const scale = Math.min(1, Math.max(0, (rawRatio - LOW) / (HIGH - LOW)));
const buffer = size * AUTOCOMPACT_BUFFER_PERCENT * scale;
return Math.min(100, Math.round(((totalTokens + buffer) / size) * 100));
}
export function getModelName(stdin) {
const displayName = stdin.model?.display_name?.trim();
if (displayName) {
return displayName;
}
const modelId = stdin.model?.id?.trim();
if (!modelId) {
return 'Unknown';
}
const normalizedBedrockLabel = normalizeBedrockModelLabel(modelId);
return normalizedBedrockLabel ?? modelId;
}
export function isBedrockModelId(modelId) {
if (!modelId) {
return false;
}
const normalized = modelId.toLowerCase();
return normalized.includes('anthropic.claude-');
}
export function getProviderLabel(stdin) {
if (isBedrockModelId(stdin.model?.id)) {
return 'Bedrock';
}
return null;
}
function parseRateLimitPercent(value) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return null;
}
return Math.round(Math.min(100, Math.max(0, value)));
}
function parseRateLimitResetAt(value) {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
return null;
}
return new Date(value * 1000);
}
export function getUsageFromStdin(stdin) {
const rateLimits = stdin.rate_limits;
if (!rateLimits) {
return null;
}
const fiveHour = parseRateLimitPercent(rateLimits.five_hour?.used_percentage);
const sevenDay = parseRateLimitPercent(rateLimits.seven_day?.used_percentage);
if (fiveHour === null && sevenDay === null) {
return null;
}
return {
fiveHour,
sevenDay,
fiveHourResetAt: parseRateLimitResetAt(rateLimits.five_hour?.resets_at),
sevenDayResetAt: parseRateLimitResetAt(rateLimits.seven_day?.resets_at),
};
}
/**
* Strips redundant context-window size suffixes from model display names.
*
* Claude Code may include the context window size in the display name
* (e.g. "Opus 4.6 (1M context)"), but the HUD already shows context
* usage via the context bar — so the parenthetical is redundant.
*/
export function stripContextSuffix(name) {
return name.replace(/\s*\([^)]*\bcontext\b[^)]*\)/i, '').trim();
}
/**
* Formats a model name according to the user's chosen display settings.
*
* When `override` is set, it replaces the model name entirely.
* Otherwise, `format` controls how the raw name is abbreviated:
*
* full: Return raw name unchanged (e.g. "Opus 4.6 (1M context)")
* compact: Strip context-window suffix (e.g. "Opus 4.6")
* short: Strip context suffix AND leading "Claude " prefix (e.g. "Opus 4.6")
*/
export function formatModelName(name, format, override) {
if (override) {
return override;
}
if (!format || format === 'full') {
return name;
}
let result = stripContextSuffix(name);
if (format === 'short') {
result = result.replace(/^Claude\s+/i, '');
}
return result;
}
function normalizeBedrockModelLabel(modelId) {
if (!isBedrockModelId(modelId)) {
return null;
}
const lowercaseId = modelId.toLowerCase();
const claudePrefix = 'anthropic.claude-';
const claudeIndex = lowercaseId.indexOf(claudePrefix);
if (claudeIndex === -1) {
return null;
}
let suffix = lowercaseId.slice(claudeIndex + claudePrefix.length);
suffix = suffix.replace(/-v\d+:\d+$/, '');
suffix = suffix.replace(/-\d{8}$/, '');
const tokens = suffix.split('-').filter(Boolean);
if (tokens.length === 0) {
return null;
}
const familyIndex = tokens.findIndex((token) => token === 'haiku' || token === 'sonnet' || token === 'opus');
if (familyIndex === -1) {
return null;
}
const family = tokens[familyIndex];
const beforeVersion = readNumericVersion(tokens, familyIndex - 1, -1).reverse();
const afterVersion = readNumericVersion(tokens, familyIndex + 1, 1);
const versionParts = beforeVersion.length >= afterVersion.length ? beforeVersion : afterVersion;
const version = versionParts.length ? versionParts.join('.') : null;
const familyLabel = family[0].toUpperCase() + family.slice(1);
return version ? `Claude ${familyLabel} ${version}` : `Claude ${familyLabel}`;
}
function readNumericVersion(tokens, startIndex, step) {
const parts = [];
for (let i = startIndex; i >= 0 && i < tokens.length; i += step) {
if (!/^\d+$/.test(tokens[i])) {
break;
}
parts.push(tokens[i]);
if (parts.length === 2) {
break;
}
}
return parts;
}
//# sourceMappingURL=stdin.js.map