mirror of
https://github.com/jarrodwatts/claude-hud.git
synced 2026-05-21 15:52:37 +00:00
* docs(setup): handle win32 bash vs powershell command paths * fix(stdin): normalize bedrock model ids when display_name is missing * fix(usage): harden proxy oauth fallback and timeout handling
162 lines
4.6 KiB
TypeScript
162 lines
4.6 KiB
TypeScript
import type { StdinData } from './types.js';
|
|
import { AUTOCOMPACT_BUFFER_PERCENT } from './constants.js';
|
|
|
|
export async function readStdin(): Promise<StdinData | null> {
|
|
if (process.stdin.isTTY) {
|
|
return null;
|
|
}
|
|
|
|
const chunks: string[] = [];
|
|
|
|
try {
|
|
process.stdin.setEncoding('utf8');
|
|
for await (const chunk of process.stdin) {
|
|
chunks.push(chunk as string);
|
|
}
|
|
const raw = chunks.join('');
|
|
if (!raw.trim()) {
|
|
return null;
|
|
}
|
|
return JSON.parse(raw) as StdinData;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function getTotalTokens(stdin: StdinData): number {
|
|
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: StdinData): number | null {
|
|
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: StdinData): number {
|
|
// 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: StdinData): number {
|
|
// Prefer native percentage (v2.1.6+) - accurate and matches /context
|
|
// Native percentage already accounts for context correctly, no buffer needed
|
|
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);
|
|
const buffer = size * AUTOCOMPACT_BUFFER_PERCENT;
|
|
return Math.min(100, Math.round(((totalTokens + buffer) / size) * 100));
|
|
}
|
|
|
|
export function getModelName(stdin: StdinData): string {
|
|
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?: string): boolean {
|
|
if (!modelId) {
|
|
return false;
|
|
}
|
|
const normalized = modelId.toLowerCase();
|
|
return normalized.includes('anthropic.claude-');
|
|
}
|
|
|
|
export function getProviderLabel(stdin: StdinData): string | null {
|
|
if (isBedrockModelId(stdin.model?.id)) {
|
|
return 'Bedrock';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function normalizeBedrockModelLabel(modelId: string): string | null {
|
|
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: string[], startIndex: number, step: -1 | 1): string[] {
|
|
const parts: string[] = [];
|
|
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;
|
|
}
|