@sooneocean/claude-hud 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +20 -0
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/commands/configure.md +361 -0
- package/commands/export.md +43 -0
- package/commands/health.md +61 -0
- package/commands/setup.md +287 -0
- package/commands/theme.md +31 -0
- package/dist/alert.d.ts +31 -0
- package/dist/alert.d.ts.map +1 -0
- package/dist/alert.js +53 -0
- package/dist/alert.js.map +1 -0
- package/dist/burn-rate.d.ts +4 -0
- package/dist/burn-rate.d.ts.map +1 -0
- package/dist/burn-rate.js +36 -0
- package/dist/burn-rate.js.map +1 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +47 -0
- package/dist/cache.js.map +1 -0
- package/dist/claude-config-dir.d.ts +4 -0
- package/dist/claude-config-dir.d.ts.map +1 -0
- package/dist/claude-config-dir.js +24 -0
- package/dist/claude-config-dir.js.map +1 -0
- package/dist/config-io.d.ts +6 -0
- package/dist/config-io.d.ts.map +1 -0
- package/dist/config-io.js +27 -0
- package/dist/config-io.js.map +1 -0
- package/dist/config-reader.d.ts +8 -0
- package/dist/config-reader.d.ts.map +1 -0
- package/dist/config-reader.js +204 -0
- package/dist/config-reader.js.map +1 -0
- package/dist/config.d.ts +94 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +358 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/cost-tracker.d.ts +9 -0
- package/dist/cost-tracker.d.ts.map +1 -0
- package/dist/cost-tracker.js +46 -0
- package/dist/cost-tracker.js.map +1 -0
- package/dist/debug.d.ts +6 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +15 -0
- package/dist/debug.js.map +1 -0
- package/dist/extra-cmd.d.ts +20 -0
- package/dist/extra-cmd.d.ts.map +1 -0
- package/dist/extra-cmd.js +112 -0
- package/dist/extra-cmd.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +94 -0
- package/dist/git.js.map +1 -0
- package/dist/health-check.d.ts +12 -0
- package/dist/health-check.d.ts.map +1 -0
- package/dist/health-check.js +37 -0
- package/dist/health-check.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/agent-teams-provider.d.ts +10 -0
- package/dist/providers/agent-teams-provider.d.ts.map +1 -0
- package/dist/providers/agent-teams-provider.js +57 -0
- package/dist/providers/agent-teams-provider.js.map +1 -0
- package/dist/providers/agw-provider.d.ts +10 -0
- package/dist/providers/agw-provider.d.ts.map +1 -0
- package/dist/providers/agw-provider.js +49 -0
- package/dist/providers/agw-provider.js.map +1 -0
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +25 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/render/agents-line.d.ts +3 -0
- package/dist/render/agents-line.d.ts.map +1 -0
- package/dist/render/agents-line.js +40 -0
- package/dist/render/agents-line.js.map +1 -0
- package/dist/render/alert-line.d.ts +3 -0
- package/dist/render/alert-line.d.ts.map +1 -0
- package/dist/render/alert-line.js +11 -0
- package/dist/render/alert-line.js.map +1 -0
- package/dist/render/colors.d.ts +39 -0
- package/dist/render/colors.d.ts.map +1 -0
- package/dist/render/colors.js +109 -0
- package/dist/render/colors.js.map +1 -0
- package/dist/render/framework-line.d.ts +3 -0
- package/dist/render/framework-line.d.ts.map +1 -0
- package/dist/render/framework-line.js +32 -0
- package/dist/render/framework-line.js.map +1 -0
- package/dist/render/index.d.ts +3 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +435 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/lines/environment.d.ts +3 -0
- package/dist/render/lines/environment.d.ts.map +1 -0
- package/dist/render/lines/environment.js +30 -0
- package/dist/render/lines/environment.js.map +1 -0
- package/dist/render/lines/identity.d.ts +3 -0
- package/dist/render/lines/identity.d.ts.map +1 -0
- package/dist/render/lines/identity.js +93 -0
- package/dist/render/lines/identity.js.map +1 -0
- package/dist/render/lines/index.d.ts +5 -0
- package/dist/render/lines/index.d.ts.map +1 -0
- package/dist/render/lines/index.js +5 -0
- package/dist/render/lines/index.js.map +1 -0
- package/dist/render/lines/project.d.ts +3 -0
- package/dist/render/lines/project.d.ts.map +1 -0
- package/dist/render/lines/project.js +100 -0
- package/dist/render/lines/project.js.map +1 -0
- package/dist/render/lines/usage.d.ts +3 -0
- package/dist/render/lines/usage.d.ts.map +1 -0
- package/dist/render/lines/usage.js +65 -0
- package/dist/render/lines/usage.js.map +1 -0
- package/dist/render/session-line.d.ts +7 -0
- package/dist/render/session-line.d.ts.map +1 -0
- package/dist/render/session-line.js +227 -0
- package/dist/render/session-line.js.map +1 -0
- package/dist/render/todos-line.d.ts +3 -0
- package/dist/render/todos-line.d.ts.map +1 -0
- package/dist/render/todos-line.js +29 -0
- package/dist/render/todos-line.js.map +1 -0
- package/dist/render/tools-line.d.ts +3 -0
- package/dist/render/tools-line.d.ts.map +1 -0
- package/dist/render/tools-line.js +45 -0
- package/dist/render/tools-line.js.map +1 -0
- package/dist/session-history.d.ts +15 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +46 -0
- package/dist/session-history.js.map +1 -0
- package/dist/session-stats.d.ts +11 -0
- package/dist/session-stats.d.ts.map +1 -0
- package/dist/session-stats.js +48 -0
- package/dist/session-stats.js.map +1 -0
- package/dist/speed-tracker.d.ts +7 -0
- package/dist/speed-tracker.d.ts.map +1 -0
- package/dist/speed-tracker.js +34 -0
- package/dist/speed-tracker.js.map +1 -0
- package/dist/stdin.d.ts +9 -0
- package/dist/stdin.d.ts.map +1 -0
- package/dist/stdin.js +142 -0
- package/dist/stdin.js.map +1 -0
- package/dist/themes.d.ts +10 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +81 -0
- package/dist/themes.js.map +1 -0
- package/dist/transcript.d.ts +3 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +221 -0
- package/dist/transcript.js.map +1 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-api.d.ts +62 -0
- package/dist/usage-api.d.ts.map +1 -0
- package/dist/usage-api.js +908 -0
- package/dist/usage-api.js.map +1 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +75 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/terminal.d.ts +5 -0
- package/dist/utils/terminal.d.ts.map +1 -0
- package/dist/utils/terminal.js +42 -0
- package/dist/utils/terminal.js.map +1 -0
- package/package.json +36 -0
- package/src/alert.ts +75 -0
- package/src/burn-rate.ts +45 -0
- package/src/cache.ts +57 -0
- package/src/claude-config-dir.ts +27 -0
- package/src/config-io.ts +26 -0
- package/src/config-reader.ts +236 -0
- package/src/config.ts +496 -0
- package/src/constants.ts +10 -0
- package/src/cost-tracker.ts +53 -0
- package/src/debug.ts +16 -0
- package/src/extra-cmd.ts +125 -0
- package/src/git.ts +126 -0
- package/src/health-check.ts +50 -0
- package/src/index.ts +234 -0
- package/src/providers/agent-teams-provider.ts +56 -0
- package/src/providers/agw-provider.ts +47 -0
- package/src/providers/index.ts +27 -0
- package/src/render/agents-line.ts +51 -0
- package/src/render/alert-line.ts +11 -0
- package/src/render/colors.ts +145 -0
- package/src/render/framework-line.ts +34 -0
- package/src/render/index.ts +512 -0
- package/src/render/lines/environment.ts +41 -0
- package/src/render/lines/identity.ts +109 -0
- package/src/render/lines/index.ts +4 -0
- package/src/render/lines/project.ts +113 -0
- package/src/render/lines/usage.ts +79 -0
- package/src/render/session-line.ts +253 -0
- package/src/render/todos-line.ts +35 -0
- package/src/render/tools-line.ts +58 -0
- package/src/session-history.ts +62 -0
- package/src/session-stats.ts +65 -0
- package/src/speed-tracker.ts +51 -0
- package/src/stdin.ts +169 -0
- package/src/themes.ts +90 -0
- package/src/transcript.ts +268 -0
- package/src/types.ts +146 -0
- package/src/usage-api.ts +1090 -0
- package/src/utils/format.ts +79 -0
- package/src/utils/terminal.ts +46 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RenderContext } from '../types.js';
|
|
2
|
+
import { yellow, green, cyan, dim, magenta, red } from './colors.js';
|
|
3
|
+
import { truncatePath } from '../utils/format.js';
|
|
4
|
+
|
|
5
|
+
export function renderToolsLine(ctx: RenderContext): string | null {
|
|
6
|
+
const { tools } = ctx.transcript;
|
|
7
|
+
|
|
8
|
+
if (tools.length === 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const parts: string[] = [];
|
|
13
|
+
|
|
14
|
+
const runningTools = tools.filter((t) => t.status === 'running');
|
|
15
|
+
const completedTools = tools.filter((t) => t.status === 'completed' || t.status === 'error');
|
|
16
|
+
|
|
17
|
+
for (const tool of runningTools.slice(-2)) {
|
|
18
|
+
const target = tool.target ? truncatePath(tool.target.replace(/\\/g, '/')) : '';
|
|
19
|
+
parts.push(`${yellow('◐')} ${cyan(tool.name)}${target ? dim(`: ${target}`) : ''}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toolCounts = new Map<string, number>();
|
|
23
|
+
for (const tool of completedTools) {
|
|
24
|
+
const count = toolCounts.get(tool.name) ?? 0;
|
|
25
|
+
toolCounts.set(tool.name, count + 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sortedTools = Array.from(toolCounts.entries())
|
|
29
|
+
.sort((a, b) => b[1] - a[1])
|
|
30
|
+
.slice(0, 4);
|
|
31
|
+
|
|
32
|
+
for (const [name, count] of sortedTools) {
|
|
33
|
+
parts.push(`${green('✓')} ${name} ${dim(`×${count}`)}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Show error count if any tools failed
|
|
37
|
+
const errorCount = ctx.transcript.tools.filter(t => t.status === 'error').length;
|
|
38
|
+
if (errorCount > 0) {
|
|
39
|
+
parts.push(`${red('✘')} ${errorCount} err`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (ctx.config.display.mergeToolsAgents && ctx.transcript.agents.length > 0) {
|
|
43
|
+
const recentAgents = ctx.transcript.agents.slice(-2);
|
|
44
|
+
for (const agent of recentAgents) {
|
|
45
|
+
const icon = agent.status === 'running' ? magenta('◐') : green('✓');
|
|
46
|
+
const model = agent.model ? dim(`[${agent.model}]`) : '';
|
|
47
|
+
const desc = agent.description ? dim(`: ${agent.description.slice(0, 30)}`) : '';
|
|
48
|
+
parts.push(`${icon} ${agent.type || 'agent'}${model}${desc}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (parts.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return parts.join(' | ');
|
|
57
|
+
}
|
|
58
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === '*';
|
|
5
|
+
|
|
6
|
+
export interface SessionRecord {
|
|
7
|
+
startTime: string; // ISO string
|
|
8
|
+
endTime: string; // ISO string (when last seen)
|
|
9
|
+
duration: string; // formatted "1h 23m"
|
|
10
|
+
model: string;
|
|
11
|
+
peakContextPercent: number;
|
|
12
|
+
autocompactCount: number;
|
|
13
|
+
totalToolCalls: number;
|
|
14
|
+
totalAgentRuns: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getHistoryPath(): string {
|
|
18
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(process.env.HOME || '', '.claude');
|
|
19
|
+
return path.join(configDir, 'plugins', 'claude-hud', 'session-history.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadHistory(): SessionRecord[] {
|
|
23
|
+
try {
|
|
24
|
+
const data = fs.readFileSync(getHistoryPath(), 'utf-8');
|
|
25
|
+
return JSON.parse(data);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (DEBUG) console.error('[claude-hud:session-history] file read error:', err);
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveCurrentSession(record: Omit<SessionRecord, 'endTime'>): void {
|
|
33
|
+
const history = loadHistory();
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
|
|
36
|
+
// Check if this is an update to the current session (same startTime)
|
|
37
|
+
const existingIdx = history.findIndex(r => r.startTime === record.startTime);
|
|
38
|
+
const fullRecord: SessionRecord = { ...record, endTime: now };
|
|
39
|
+
|
|
40
|
+
if (existingIdx >= 0) {
|
|
41
|
+
history[existingIdx] = fullRecord;
|
|
42
|
+
} else {
|
|
43
|
+
history.push(fullRecord);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Keep last 50 sessions
|
|
47
|
+
while (history.length > 50) history.shift();
|
|
48
|
+
|
|
49
|
+
const dir = path.dirname(getHistoryPath());
|
|
50
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
51
|
+
fs.writeFileSync(getHistoryPath(), JSON.stringify(history, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getLastSession(): SessionRecord | null {
|
|
55
|
+
const history = loadHistory();
|
|
56
|
+
// Return the second-to-last (last completed session, not current)
|
|
57
|
+
return history.length >= 2 ? history[history.length - 2] : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatSessionSummary(record: SessionRecord): string {
|
|
61
|
+
return `Last: ${record.model} ${record.duration} | ${record.peakContextPercent}% peak | ${record.totalToolCalls} tools | ${record.autocompactCount} compacts`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readCache, writeCache } from './cache.js';
|
|
2
|
+
import type { SessionStats } from './types.js';
|
|
3
|
+
|
|
4
|
+
const CACHE_KEY = 'session-stats';
|
|
5
|
+
const HISTORY_KEY = 'context-history';
|
|
6
|
+
const SPARKLINE_KEY = 'context-sparkline';
|
|
7
|
+
const TTL = 24 * 60 * 60 * 1000;
|
|
8
|
+
const DROP_THRESHOLD = 20;
|
|
9
|
+
const SPARKLINE_MAX = 20;
|
|
10
|
+
|
|
11
|
+
interface ContextHistory {
|
|
12
|
+
values: number[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UpdateInput {
|
|
16
|
+
contextPercent: number;
|
|
17
|
+
toolCount: number;
|
|
18
|
+
agentCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getSparkline(cacheDir: string): number[] {
|
|
22
|
+
return readCache<number[]>(SPARKLINE_KEY, TTL, cacheDir) ?? [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getSessionStats(cacheDir: string): SessionStats {
|
|
26
|
+
const cached = readCache<SessionStats>(CACHE_KEY, TTL, cacheDir);
|
|
27
|
+
return cached ?? {
|
|
28
|
+
totalToolCalls: 0,
|
|
29
|
+
totalAgentRuns: 0,
|
|
30
|
+
peakContextPercent: 0,
|
|
31
|
+
autocompactCount: 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function updateSessionStats(cacheDir: string, input: UpdateInput): void {
|
|
36
|
+
const stats = getSessionStats(cacheDir);
|
|
37
|
+
const history = readCache<ContextHistory>(HISTORY_KEY, TTL, cacheDir) ?? { values: [] };
|
|
38
|
+
|
|
39
|
+
stats.totalToolCalls = input.toolCount;
|
|
40
|
+
stats.totalAgentRuns = input.agentCount;
|
|
41
|
+
if (input.contextPercent > stats.peakContextPercent) {
|
|
42
|
+
stats.peakContextPercent = input.contextPercent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
history.values.push(input.contextPercent);
|
|
46
|
+
if (history.values.length > 3) history.values.shift();
|
|
47
|
+
|
|
48
|
+
if (history.values.length >= 3) {
|
|
49
|
+
const [prev2, prev1, current] = history.values.slice(-3);
|
|
50
|
+
const dropFromPrev2 = prev2 - prev1;
|
|
51
|
+
const sustainedDrop = prev2 - current;
|
|
52
|
+
if (dropFromPrev2 > DROP_THRESHOLD && sustainedDrop > DROP_THRESHOLD) {
|
|
53
|
+
stats.autocompactCount++;
|
|
54
|
+
history.values = [current];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sparkline = readCache<number[]>(SPARKLINE_KEY, TTL, cacheDir) ?? [];
|
|
59
|
+
sparkline.push(input.contextPercent);
|
|
60
|
+
if (sparkline.length > SPARKLINE_MAX) sparkline.shift();
|
|
61
|
+
writeCache(SPARKLINE_KEY, sparkline, cacheDir);
|
|
62
|
+
|
|
63
|
+
writeCache(CACHE_KEY, stats, cacheDir);
|
|
64
|
+
writeCache(HISTORY_KEY, history, cacheDir);
|
|
65
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type { StdinData } from './types.js';
|
|
4
|
+
import { readCache, writeCache } from './cache.js';
|
|
5
|
+
import { getHudPluginDir } from './claude-config-dir.js';
|
|
6
|
+
|
|
7
|
+
const SPEED_WINDOW_MS = 2000;
|
|
8
|
+
const SPEED_CACHE_KEY = 'speed-tracker';
|
|
9
|
+
|
|
10
|
+
interface SpeedCache {
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SpeedTrackerDeps = {
|
|
16
|
+
homeDir: () => string;
|
|
17
|
+
now: () => number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const defaultDeps: SpeedTrackerDeps = {
|
|
21
|
+
homeDir: () => os.homedir(),
|
|
22
|
+
now: () => Date.now(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getCacheDir(homeDir: string): string {
|
|
26
|
+
return path.join(getHudPluginDir(homeDir), '.cache');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getOutputSpeed(stdin: StdinData, overrides: Partial<SpeedTrackerDeps> = {}): number | null {
|
|
30
|
+
const outputTokens = stdin.context_window?.current_usage?.output_tokens;
|
|
31
|
+
if (typeof outputTokens !== 'number' || !Number.isFinite(outputTokens)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const deps = { ...defaultDeps, ...overrides };
|
|
36
|
+
const now = deps.now();
|
|
37
|
+
const cacheDir = getCacheDir(deps.homeDir());
|
|
38
|
+
const previous = readCache<SpeedCache>(SPEED_CACHE_KEY, 5000, cacheDir);
|
|
39
|
+
|
|
40
|
+
let speed: number | null = null;
|
|
41
|
+
if (previous && outputTokens >= previous.outputTokens) {
|
|
42
|
+
const deltaTokens = outputTokens - previous.outputTokens;
|
|
43
|
+
const deltaMs = now - previous.timestamp;
|
|
44
|
+
if (deltaTokens > 0 && deltaMs > 0 && deltaMs <= SPEED_WINDOW_MS) {
|
|
45
|
+
speed = deltaTokens / (deltaMs / 1000);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeCache<SpeedCache>(SPEED_CACHE_KEY, { outputTokens, timestamp: now }, cacheDir);
|
|
50
|
+
return speed;
|
|
51
|
+
}
|
package/src/stdin.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { StdinData } from './types.js';
|
|
2
|
+
import { AUTOCOMPACT_BUFFER_PERCENT } from './constants.js';
|
|
3
|
+
|
|
4
|
+
export async function readStdin(): Promise<StdinData | null> {
|
|
5
|
+
if (process.stdin.isTTY) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const chunks: string[] = [];
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
process.stdin.setEncoding('utf8');
|
|
13
|
+
for await (const chunk of process.stdin) {
|
|
14
|
+
chunks.push(chunk as string);
|
|
15
|
+
}
|
|
16
|
+
const raw = chunks.join('');
|
|
17
|
+
if (!raw.trim()) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(raw) as StdinData;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getTotalTokens(stdin: StdinData): number {
|
|
27
|
+
const usage = stdin.context_window?.current_usage;
|
|
28
|
+
return (
|
|
29
|
+
(usage?.input_tokens ?? 0) +
|
|
30
|
+
(usage?.cache_creation_input_tokens ?? 0) +
|
|
31
|
+
(usage?.cache_read_input_tokens ?? 0)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get native percentage from Claude Code v2.1.6+ if available.
|
|
37
|
+
* Returns null if not available or invalid, triggering fallback to manual calculation.
|
|
38
|
+
*/
|
|
39
|
+
function getNativePercent(stdin: StdinData): number | null {
|
|
40
|
+
const nativePercent = stdin.context_window?.used_percentage;
|
|
41
|
+
if (typeof nativePercent === 'number' && !Number.isNaN(nativePercent)) {
|
|
42
|
+
return Math.min(100, Math.max(0, Math.round(nativePercent)));
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getContextPercent(stdin: StdinData): number {
|
|
48
|
+
// Prefer native percentage (v2.1.6+) - accurate and matches /context
|
|
49
|
+
const native = getNativePercent(stdin);
|
|
50
|
+
if (native !== null) {
|
|
51
|
+
return native;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback: manual calculation without buffer
|
|
55
|
+
const size = stdin.context_window?.context_window_size;
|
|
56
|
+
if (!size || size <= 0) {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const totalTokens = getTotalTokens(stdin);
|
|
61
|
+
return Math.min(100, Math.round((totalTokens / size) * 100));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getBufferedPercent(stdin: StdinData): number {
|
|
65
|
+
// Prefer native percentage (v2.1.6+) so the HUD matches Claude Code's
|
|
66
|
+
// own context output. The buffered fallback only approximates older versions.
|
|
67
|
+
const native = getNativePercent(stdin);
|
|
68
|
+
if (native !== null) {
|
|
69
|
+
return native;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback: manual calculation with buffer for older Claude Code versions
|
|
73
|
+
const size = stdin.context_window?.context_window_size;
|
|
74
|
+
if (!size || size <= 0) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const totalTokens = getTotalTokens(stdin);
|
|
79
|
+
|
|
80
|
+
// Scale buffer by raw usage: no buffer at ≤5% (e.g. after /clear),
|
|
81
|
+
// full buffer at ≥50%. Autocompact doesn't kick in at very low usage.
|
|
82
|
+
const rawRatio = totalTokens / size;
|
|
83
|
+
const LOW = 0.05;
|
|
84
|
+
const HIGH = 0.50;
|
|
85
|
+
const scale = Math.min(1, Math.max(0, (rawRatio - LOW) / (HIGH - LOW)));
|
|
86
|
+
const buffer = size * AUTOCOMPACT_BUFFER_PERCENT * scale;
|
|
87
|
+
|
|
88
|
+
return Math.min(100, Math.round(((totalTokens + buffer) / size) * 100));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getModelName(stdin: StdinData): string {
|
|
92
|
+
const displayName = stdin.model?.display_name?.trim();
|
|
93
|
+
if (displayName) {
|
|
94
|
+
return displayName;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const modelId = stdin.model?.id?.trim();
|
|
98
|
+
if (!modelId) {
|
|
99
|
+
return 'Unknown';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const normalizedBedrockLabel = normalizeBedrockModelLabel(modelId);
|
|
103
|
+
return normalizedBedrockLabel ?? modelId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isBedrockModelId(modelId?: string): boolean {
|
|
107
|
+
if (!modelId) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const normalized = modelId.toLowerCase();
|
|
111
|
+
return normalized.includes('anthropic.claude-');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getProviderLabel(stdin: StdinData): string | null {
|
|
115
|
+
if (isBedrockModelId(stdin.model?.id)) {
|
|
116
|
+
return 'Bedrock';
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeBedrockModelLabel(modelId: string): string | null {
|
|
122
|
+
if (!isBedrockModelId(modelId)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const lowercaseId = modelId.toLowerCase();
|
|
127
|
+
const claudePrefix = 'anthropic.claude-';
|
|
128
|
+
const claudeIndex = lowercaseId.indexOf(claudePrefix);
|
|
129
|
+
if (claudeIndex === -1) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let suffix = lowercaseId.slice(claudeIndex + claudePrefix.length);
|
|
134
|
+
suffix = suffix.replace(/-v\d+:\d+$/, '');
|
|
135
|
+
suffix = suffix.replace(/-\d{8}$/, '');
|
|
136
|
+
|
|
137
|
+
const tokens = suffix.split('-').filter(Boolean);
|
|
138
|
+
if (tokens.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const familyIndex = tokens.findIndex((token) => token === 'haiku' || token === 'sonnet' || token === 'opus');
|
|
143
|
+
if (familyIndex === -1) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const family = tokens[familyIndex];
|
|
148
|
+
const beforeVersion = readNumericVersion(tokens, familyIndex - 1, -1).reverse();
|
|
149
|
+
const afterVersion = readNumericVersion(tokens, familyIndex + 1, 1);
|
|
150
|
+
const versionParts = beforeVersion.length >= afterVersion.length ? beforeVersion : afterVersion;
|
|
151
|
+
const version = versionParts.length ? versionParts.join('.') : null;
|
|
152
|
+
const familyLabel = family[0].toUpperCase() + family.slice(1);
|
|
153
|
+
|
|
154
|
+
return version ? `Claude ${familyLabel} ${version}` : `Claude ${familyLabel}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readNumericVersion(tokens: string[], startIndex: number, step: -1 | 1): string[] {
|
|
158
|
+
const parts: string[] = [];
|
|
159
|
+
for (let i = startIndex; i >= 0 && i < tokens.length; i += step) {
|
|
160
|
+
if (!/^\d+$/.test(tokens[i])) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
parts.push(tokens[i]);
|
|
164
|
+
if (parts.length === 2) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return parts;
|
|
169
|
+
}
|
package/src/themes.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { HudColorOverrides } from './config.js';
|
|
2
|
+
|
|
3
|
+
export interface ColorTheme {
|
|
4
|
+
name: string;
|
|
5
|
+
colors: HudColorOverrides;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const THEMES: Record<string, ColorTheme> = {
|
|
9
|
+
default: {
|
|
10
|
+
name: 'Default',
|
|
11
|
+
colors: {
|
|
12
|
+
context: 'green',
|
|
13
|
+
usage: 'brightBlue',
|
|
14
|
+
warning: 'yellow',
|
|
15
|
+
usageWarning: 'brightMagenta',
|
|
16
|
+
critical: 'red',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
catppuccin: {
|
|
20
|
+
name: 'Catppuccin Mocha',
|
|
21
|
+
colors: {
|
|
22
|
+
context: '#a6e3a1', // green
|
|
23
|
+
usage: '#89b4fa', // blue
|
|
24
|
+
warning: '#f9e2af', // yellow
|
|
25
|
+
usageWarning: '#cba6f7', // mauve
|
|
26
|
+
critical: '#f38ba8', // red
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
dracula: {
|
|
30
|
+
name: 'Dracula',
|
|
31
|
+
colors: {
|
|
32
|
+
context: '#50fa7b', // green
|
|
33
|
+
usage: '#8be9fd', // cyan
|
|
34
|
+
warning: '#f1fa8c', // yellow
|
|
35
|
+
usageWarning: '#bd93f9', // purple
|
|
36
|
+
critical: '#ff5555', // red
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
nord: {
|
|
40
|
+
name: 'Nord',
|
|
41
|
+
colors: {
|
|
42
|
+
context: '#a3be8c', // green
|
|
43
|
+
usage: '#81a1c1', // blue
|
|
44
|
+
warning: '#ebcb8b', // yellow
|
|
45
|
+
usageWarning: '#b48ead', // purple
|
|
46
|
+
critical: '#bf616a', // red
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
catppuccin_latte: {
|
|
50
|
+
name: 'Catppuccin Latte',
|
|
51
|
+
colors: {
|
|
52
|
+
context: '#40a02b', // green
|
|
53
|
+
usage: '#1e66f5', // blue
|
|
54
|
+
warning: '#df8e1d', // yellow
|
|
55
|
+
usageWarning: '#8839ef', // mauve
|
|
56
|
+
critical: '#d20f39', // red
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
nord_light: {
|
|
60
|
+
name: 'Nord Light',
|
|
61
|
+
colors: {
|
|
62
|
+
context: '#a3be8c', // green (same as dark)
|
|
63
|
+
usage: '#5e81ac', // blue
|
|
64
|
+
warning: '#ebcb8b', // yellow (same as dark)
|
|
65
|
+
usageWarning: '#b48ead', // purple (same as dark)
|
|
66
|
+
critical: '#bf616a', // red (same as dark)
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function getTheme(name: string): ColorTheme | undefined {
|
|
72
|
+
return THEMES[name.toLowerCase()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getThemeNames(): string[] {
|
|
76
|
+
return Object.keys(THEMES);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function detectTerminalTheme(): 'dark' | 'light' | null {
|
|
80
|
+
// COLORFGBG format: "foreground;background" (e.g., "15;0" = white on black = dark)
|
|
81
|
+
const colorfgbg = process.env.COLORFGBG;
|
|
82
|
+
if (!colorfgbg) return null;
|
|
83
|
+
|
|
84
|
+
const parts = colorfgbg.split(';');
|
|
85
|
+
const bg = parseInt(parts[parts.length - 1], 10);
|
|
86
|
+
if (isNaN(bg)) return null;
|
|
87
|
+
|
|
88
|
+
// Low background color = dark theme, high = light theme
|
|
89
|
+
return bg < 8 ? 'dark' : 'light';
|
|
90
|
+
}
|