@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
package/src/git.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { readCache, writeCache, getDefaultCacheDir } from './cache.js';
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export interface FileStats {
|
|
8
|
+
modified: number;
|
|
9
|
+
added: number;
|
|
10
|
+
deleted: number;
|
|
11
|
+
untracked: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GitStatus {
|
|
15
|
+
branch: string;
|
|
16
|
+
isDirty: boolean;
|
|
17
|
+
ahead: number;
|
|
18
|
+
behind: number;
|
|
19
|
+
fileStats?: FileStats;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function getGitBranch(cwd?: string): Promise<string | null> {
|
|
23
|
+
if (!cwd) return null;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const { stdout } = await execFileAsync(
|
|
27
|
+
'git',
|
|
28
|
+
['rev-parse', '--abbrev-ref', 'HEAD'],
|
|
29
|
+
{ cwd, timeout: 1000, encoding: 'utf8' }
|
|
30
|
+
);
|
|
31
|
+
return stdout.trim() || null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getGitStatus(cwd?: string): Promise<GitStatus | null> {
|
|
38
|
+
if (!cwd) return null;
|
|
39
|
+
|
|
40
|
+
const cacheDir = getDefaultCacheDir();
|
|
41
|
+
const cacheKey = `git-status:${cwd}`;
|
|
42
|
+
const cached = readCache<GitStatus>(cacheKey, 2000, cacheDir);
|
|
43
|
+
if (cached) return cached;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Get branch name
|
|
47
|
+
const { stdout: branchOut } = await execFileAsync(
|
|
48
|
+
'git',
|
|
49
|
+
['rev-parse', '--abbrev-ref', 'HEAD'],
|
|
50
|
+
{ cwd, timeout: 1000, encoding: 'utf8' }
|
|
51
|
+
);
|
|
52
|
+
const branch = branchOut.trim();
|
|
53
|
+
if (!branch) return null;
|
|
54
|
+
|
|
55
|
+
// Check for dirty state and parse file stats
|
|
56
|
+
let isDirty = false;
|
|
57
|
+
let fileStats: FileStats | undefined;
|
|
58
|
+
try {
|
|
59
|
+
const { stdout: statusOut } = await execFileAsync(
|
|
60
|
+
'git',
|
|
61
|
+
['--no-optional-locks', 'status', '--porcelain'],
|
|
62
|
+
{ cwd, timeout: 1000, encoding: 'utf8' }
|
|
63
|
+
);
|
|
64
|
+
const trimmed = statusOut.trim();
|
|
65
|
+
isDirty = trimmed.length > 0;
|
|
66
|
+
if (isDirty) {
|
|
67
|
+
fileStats = parseFileStats(trimmed);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore errors, assume clean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get ahead/behind counts
|
|
74
|
+
let ahead = 0;
|
|
75
|
+
let behind = 0;
|
|
76
|
+
try {
|
|
77
|
+
const { stdout: revOut } = await execFileAsync(
|
|
78
|
+
'git',
|
|
79
|
+
['rev-list', '--left-right', '--count', '@{upstream}...HEAD'],
|
|
80
|
+
{ cwd, timeout: 1000, encoding: 'utf8' }
|
|
81
|
+
);
|
|
82
|
+
const parts = revOut.trim().split(/\s+/);
|
|
83
|
+
if (parts.length === 2) {
|
|
84
|
+
behind = parseInt(parts[0], 10) || 0;
|
|
85
|
+
ahead = parseInt(parts[1], 10) || 0;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// No upstream or error, keep 0/0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result: GitStatus = { branch, isDirty, ahead, behind, fileStats };
|
|
92
|
+
writeCache(cacheKey, result, cacheDir);
|
|
93
|
+
return result;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse git status --porcelain output and count file stats (Starship-compatible format)
|
|
101
|
+
* Status codes: M=modified, A=added, D=deleted, ??=untracked
|
|
102
|
+
*/
|
|
103
|
+
function parseFileStats(porcelainOutput: string): FileStats {
|
|
104
|
+
const stats: FileStats = { modified: 0, added: 0, deleted: 0, untracked: 0 };
|
|
105
|
+
const lines = porcelainOutput.split('\n').filter(Boolean);
|
|
106
|
+
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (line.length < 2) continue;
|
|
109
|
+
|
|
110
|
+
const index = line[0]; // staged status
|
|
111
|
+
const worktree = line[1]; // unstaged status
|
|
112
|
+
|
|
113
|
+
if (line.startsWith('??')) {
|
|
114
|
+
stats.untracked++;
|
|
115
|
+
} else if (index === 'A') {
|
|
116
|
+
stats.added++;
|
|
117
|
+
} else if (index === 'D' || worktree === 'D') {
|
|
118
|
+
stats.deleted++;
|
|
119
|
+
} else if (index === 'M' || worktree === 'M' || index === 'R' || index === 'C') {
|
|
120
|
+
// M=modified, R=renamed (counts as modified), C=copied (counts as modified)
|
|
121
|
+
stats.modified++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return stats;
|
|
126
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { getConfigPath, loadConfig } from './config.js';
|
|
3
|
+
import { getDefaultCacheDir } from './cache.js';
|
|
4
|
+
|
|
5
|
+
export interface HealthStatus {
|
|
6
|
+
configExists: boolean;
|
|
7
|
+
configValid: boolean;
|
|
8
|
+
cacheDir: string;
|
|
9
|
+
cacheDirExists: boolean;
|
|
10
|
+
nodeVersion: string;
|
|
11
|
+
platform: string;
|
|
12
|
+
terminalWidth: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runHealthCheck(): Promise<HealthStatus> {
|
|
16
|
+
const configPath = getConfigPath();
|
|
17
|
+
let configValid = false;
|
|
18
|
+
const configExists = fs.existsSync(configPath);
|
|
19
|
+
|
|
20
|
+
if (configExists) {
|
|
21
|
+
try {
|
|
22
|
+
await loadConfig();
|
|
23
|
+
configValid = true;
|
|
24
|
+
} catch {
|
|
25
|
+
configValid = false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cacheDir = getDefaultCacheDir();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
configExists,
|
|
33
|
+
configValid,
|
|
34
|
+
cacheDir,
|
|
35
|
+
cacheDirExists: fs.existsSync(cacheDir),
|
|
36
|
+
nodeVersion: process.version,
|
|
37
|
+
platform: process.platform,
|
|
38
|
+
terminalWidth: process.stdout?.columns ?? null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatHealthReport(status: HealthStatus): string {
|
|
43
|
+
const lines = [
|
|
44
|
+
`Platform: ${status.platform} | Node: ${status.nodeVersion}`,
|
|
45
|
+
`Terminal: ${status.terminalWidth ?? 'unknown'} cols`,
|
|
46
|
+
`Config: ${status.configExists ? (status.configValid ? '✓ valid' : '✘ invalid') : '○ not set (using defaults)'}`,
|
|
47
|
+
`Cache: ${status.cacheDirExists ? '✓ exists' : '○ will be created'}`,
|
|
48
|
+
];
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readStdin, getContextPercent } from './stdin.js';
|
|
2
|
+
import { parseTranscript } from './transcript.js';
|
|
3
|
+
import { render } from './render/index.js';
|
|
4
|
+
import { countConfigs } from './config-reader.js';
|
|
5
|
+
import { getGitStatus } from './git.js';
|
|
6
|
+
import { getUsage } from './usage-api.js';
|
|
7
|
+
import { loadConfig } from './config.js';
|
|
8
|
+
import { parseExtraCmdArg, runExtraCmd } from './extra-cmd.js';
|
|
9
|
+
import type { RenderContext } from './types.js';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { realpathSync } from 'node:fs';
|
|
12
|
+
import { getDefaultCacheDir, readCache, writeCache, readLatency, writeLatency } from './cache.js';
|
|
13
|
+
import { loadProviders, fetchAllProviders } from './providers/index.js';
|
|
14
|
+
import { evaluateAlerts, shouldBell, sendNotification } from './alert.js';
|
|
15
|
+
import { calculateBurnRate, recordTokenSnapshot } from './burn-rate.js';
|
|
16
|
+
import { updateSessionStats, getSessionStats, getSparkline } from './session-stats.js';
|
|
17
|
+
import { getTerminalWidth } from './utils/terminal.js';
|
|
18
|
+
import { estimateCost } from './cost-tracker.js';
|
|
19
|
+
import { saveCurrentSession, getLastSession, formatSessionSummary } from './session-history.js';
|
|
20
|
+
import { formatResetTime } from './utils/format.js';
|
|
21
|
+
|
|
22
|
+
export type MainDeps = {
|
|
23
|
+
readStdin: typeof readStdin;
|
|
24
|
+
parseTranscript: typeof parseTranscript;
|
|
25
|
+
countConfigs: typeof countConfigs;
|
|
26
|
+
getGitStatus: typeof getGitStatus;
|
|
27
|
+
getUsage: typeof getUsage;
|
|
28
|
+
loadConfig: typeof loadConfig;
|
|
29
|
+
parseExtraCmdArg: typeof parseExtraCmdArg;
|
|
30
|
+
runExtraCmd: typeof runExtraCmd;
|
|
31
|
+
render: typeof render;
|
|
32
|
+
now: () => number;
|
|
33
|
+
log: (...args: unknown[]) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function main(overrides: Partial<MainDeps> = {}): Promise<void> {
|
|
37
|
+
const deps: MainDeps = {
|
|
38
|
+
readStdin,
|
|
39
|
+
parseTranscript,
|
|
40
|
+
countConfigs,
|
|
41
|
+
getGitStatus,
|
|
42
|
+
getUsage,
|
|
43
|
+
loadConfig,
|
|
44
|
+
parseExtraCmdArg,
|
|
45
|
+
runExtraCmd,
|
|
46
|
+
render,
|
|
47
|
+
now: () => Date.now(),
|
|
48
|
+
log: console.log,
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const stdin = await deps.readStdin();
|
|
54
|
+
|
|
55
|
+
if (!stdin) {
|
|
56
|
+
// Running without stdin - this happens during setup verification
|
|
57
|
+
const isMacOS = process.platform === 'darwin';
|
|
58
|
+
deps.log('[claude-hud] Initializing...');
|
|
59
|
+
if (isMacOS) {
|
|
60
|
+
deps.log('[claude-hud] Note: On macOS, you may need to restart Claude Code for the HUD to appear.');
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const transcriptPath = stdin.transcript_path ?? '';
|
|
66
|
+
const transcript = await deps.parseTranscript(transcriptPath);
|
|
67
|
+
|
|
68
|
+
const { claudeMdCount, rulesCount, mcpCount, hooksCount } = await deps.countConfigs(stdin.cwd);
|
|
69
|
+
|
|
70
|
+
const config = await deps.loadConfig();
|
|
71
|
+
const gitStatus = config.gitStatus.enabled
|
|
72
|
+
? await deps.getGitStatus(stdin.cwd)
|
|
73
|
+
: null;
|
|
74
|
+
|
|
75
|
+
const cacheDir = getDefaultCacheDir();
|
|
76
|
+
|
|
77
|
+
// Only fetch usage if enabled in config (replaces env var requirement)
|
|
78
|
+
const usageStart = Date.now();
|
|
79
|
+
const usageData = config.display.showUsage !== false
|
|
80
|
+
? await deps.getUsage({
|
|
81
|
+
ttls: {
|
|
82
|
+
cacheTtlMs: config.usage.cacheTtlSeconds * 1000,
|
|
83
|
+
failureCacheTtlMs: config.usage.failureCacheTtlSeconds * 1000,
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
: null;
|
|
87
|
+
if (config.display.showUsage !== false && usageData !== null) {
|
|
88
|
+
writeLatency(Date.now() - usageStart, cacheDir);
|
|
89
|
+
}
|
|
90
|
+
const apiLatency = readLatency(cacheDir);
|
|
91
|
+
|
|
92
|
+
const extraCmd = deps.parseExtraCmdArg();
|
|
93
|
+
const extraLabel = extraCmd ? await deps.runExtraCmd(extraCmd) : null;
|
|
94
|
+
|
|
95
|
+
const sessionDuration = formatSessionDuration(transcript.sessionStart, deps.now);
|
|
96
|
+
|
|
97
|
+
// Framework providers
|
|
98
|
+
let frameworkStatus: RenderContext['frameworkStatus'] = [];
|
|
99
|
+
if (config.display.showFrameworks) {
|
|
100
|
+
const providers = loadProviders(config.frameworks, cacheDir);
|
|
101
|
+
frameworkStatus = await fetchAllProviders(providers);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Burn rate
|
|
105
|
+
let burnRate: RenderContext['burnRate'] = null;
|
|
106
|
+
const inputTokens = stdin.context_window?.current_usage?.input_tokens;
|
|
107
|
+
const contextSize = stdin.context_window?.context_window_size;
|
|
108
|
+
if (config.display.showBurnRate && inputTokens != null && contextSize != null) {
|
|
109
|
+
recordTokenSnapshot(inputTokens, cacheDir);
|
|
110
|
+
burnRate = calculateBurnRate(inputTokens, contextSize, cacheDir);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Context percent
|
|
114
|
+
const contextPercent = getContextPercent(stdin);
|
|
115
|
+
|
|
116
|
+
// Session stats
|
|
117
|
+
updateSessionStats(cacheDir, {
|
|
118
|
+
contextPercent,
|
|
119
|
+
toolCount: transcript.tools.length,
|
|
120
|
+
agentCount: transcript.agents.length,
|
|
121
|
+
});
|
|
122
|
+
const sessionStats = getSessionStats(cacheDir);
|
|
123
|
+
const sparkline = getSparkline(cacheDir);
|
|
124
|
+
|
|
125
|
+
// Alerts
|
|
126
|
+
let alerts: RenderContext['alerts'] = [];
|
|
127
|
+
if (config.display.showAlerts) {
|
|
128
|
+
alerts = evaluateAlerts({
|
|
129
|
+
contextPercent,
|
|
130
|
+
usage5hPercent: usageData?.fiveHour ?? 0,
|
|
131
|
+
usage7dPercent: usageData?.sevenDay ?? 0,
|
|
132
|
+
estimatedCallsRemaining: burnRate?.estimatedCallsRemaining ?? null,
|
|
133
|
+
usageResetTime: usageData?.fiveHourResetAt
|
|
134
|
+
? formatResetTime(usageData.fiveHourResetAt)
|
|
135
|
+
: null,
|
|
136
|
+
alertConfig: config.alerts,
|
|
137
|
+
cacheDir,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (shouldBell(alerts, cacheDir)) {
|
|
141
|
+
process.stderr.write('\x07');
|
|
142
|
+
if (config.display.showNotifications) {
|
|
143
|
+
const criticalAlert = alerts.find(a => a.type.includes('critical'));
|
|
144
|
+
if (criticalAlert) {
|
|
145
|
+
sendNotification('Claude HUD', criticalAlert.message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const costEstimate = config.display.showCost ? estimateCost(stdin, cacheDir) : null;
|
|
152
|
+
|
|
153
|
+
// Session history — only save when stats have changed
|
|
154
|
+
const prevStatsHash = readCache<string>('prev-session-hash', 60000, cacheDir);
|
|
155
|
+
const currentHash = `${sessionStats.totalToolCalls}-${sessionStats.autocompactCount}-${sessionStats.peakContextPercent}`;
|
|
156
|
+
if (prevStatsHash !== currentHash) {
|
|
157
|
+
writeCache('prev-session-hash', currentHash, cacheDir);
|
|
158
|
+
saveCurrentSession({
|
|
159
|
+
startTime: transcript.sessionStart?.toISOString() || new Date().toISOString(),
|
|
160
|
+
duration: sessionDuration,
|
|
161
|
+
model: stdin.model?.display_name || 'unknown',
|
|
162
|
+
peakContextPercent: sessionStats.peakContextPercent,
|
|
163
|
+
autocompactCount: sessionStats.autocompactCount,
|
|
164
|
+
totalToolCalls: sessionStats.totalToolCalls,
|
|
165
|
+
totalAgentRuns: sessionStats.totalAgentRuns,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Last session summary — show once at session start
|
|
170
|
+
const shownLastSession = readCache<boolean>('shown-last-session', 24 * 60 * 60 * 1000, cacheDir);
|
|
171
|
+
if (!shownLastSession && transcript.tools.length === 0) {
|
|
172
|
+
const lastSession = getLastSession();
|
|
173
|
+
if (lastSession) {
|
|
174
|
+
console.error(`[claude-hud] ${formatSessionSummary(lastSession)}`);
|
|
175
|
+
writeCache('shown-last-session', true, cacheDir);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const ctx: RenderContext = {
|
|
180
|
+
stdin,
|
|
181
|
+
transcript,
|
|
182
|
+
claudeMdCount,
|
|
183
|
+
rulesCount,
|
|
184
|
+
mcpCount,
|
|
185
|
+
hooksCount,
|
|
186
|
+
sessionDuration,
|
|
187
|
+
gitStatus,
|
|
188
|
+
usageData,
|
|
189
|
+
config,
|
|
190
|
+
extraLabel,
|
|
191
|
+
frameworkStatus,
|
|
192
|
+
alerts,
|
|
193
|
+
burnRate,
|
|
194
|
+
sessionStats,
|
|
195
|
+
sparkline,
|
|
196
|
+
terminalWidth: getTerminalWidth(),
|
|
197
|
+
costEstimate,
|
|
198
|
+
apiLatency,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
deps.render(ctx);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
deps.log('[claude-hud] Error:', error instanceof Error ? error.message : 'Unknown error');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function formatSessionDuration(sessionStart?: Date, now: () => number = () => Date.now()): string {
|
|
208
|
+
if (!sessionStart) {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ms = now() - sessionStart.getTime();
|
|
213
|
+
const mins = Math.floor(ms / 60000);
|
|
214
|
+
|
|
215
|
+
if (mins < 1) return '<1m';
|
|
216
|
+
if (mins < 60) return `${mins}m`;
|
|
217
|
+
|
|
218
|
+
const hours = Math.floor(mins / 60);
|
|
219
|
+
const remainingMins = mins % 60;
|
|
220
|
+
return `${hours}h ${remainingMins}m`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
224
|
+
const argvPath = process.argv[1];
|
|
225
|
+
const isSamePath = (a: string, b: string): boolean => {
|
|
226
|
+
try {
|
|
227
|
+
return realpathSync(a) === realpathSync(b);
|
|
228
|
+
} catch {
|
|
229
|
+
return a === b;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
if (argvPath && isSamePath(argvPath, scriptPath)) {
|
|
233
|
+
void main();
|
|
234
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import type { FrameworkProvider, FrameworkStatus, FrameworkEntry } from '../types.js';
|
|
4
|
+
import { readCache, writeCache } from '../cache.js';
|
|
5
|
+
|
|
6
|
+
const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === '*';
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const CACHE_KEY = 'agent-teams-status';
|
|
10
|
+
const SUCCESS_TTL = 5000;
|
|
11
|
+
|
|
12
|
+
export class AgentTeamsProvider implements FrameworkProvider {
|
|
13
|
+
name = 'agent-teams';
|
|
14
|
+
constructor(private cacheDir: string) {}
|
|
15
|
+
|
|
16
|
+
isAvailable(): boolean { return !!process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS; }
|
|
17
|
+
|
|
18
|
+
async fetch(): Promise<FrameworkStatus | null> {
|
|
19
|
+
const cached = readCache<FrameworkStatus>(CACHE_KEY, SUCCESS_TTL, this.cacheDir);
|
|
20
|
+
if (cached) return cached;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { timeout: 1000 });
|
|
24
|
+
const worktrees = this.parseWorktrees(stdout);
|
|
25
|
+
if (worktrees.length <= 1) return null;
|
|
26
|
+
|
|
27
|
+
const entries: FrameworkEntry[] = worktrees.slice(1).map(wt => ({
|
|
28
|
+
label: wt.branch?.replace('refs/heads/', '') || 'detached',
|
|
29
|
+
status: 'running' as const,
|
|
30
|
+
detail: wt.path,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const status: FrameworkStatus = { provider: 'Teams', entries };
|
|
34
|
+
writeCache(CACHE_KEY, status, this.cacheDir);
|
|
35
|
+
return status;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (DEBUG) console.error('[claude-hud:agent-teams-provider] git worktree error:', err);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private parseWorktrees(output: string): Array<{ path: string; branch?: string }> {
|
|
43
|
+
const worktrees: Array<{ path: string; branch?: string }> = [];
|
|
44
|
+
let current: { path: string; branch?: string } | null = null;
|
|
45
|
+
for (const line of output.split('\n')) {
|
|
46
|
+
if (line.startsWith('worktree ')) {
|
|
47
|
+
if (current) worktrees.push(current);
|
|
48
|
+
current = { path: line.slice(9) };
|
|
49
|
+
} else if (line.startsWith('branch ') && current) {
|
|
50
|
+
current.branch = line.slice(7);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (current) worktrees.push(current);
|
|
54
|
+
return worktrees;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FrameworkProvider, FrameworkStatus, FrameworkEntry } from '../types.js';
|
|
2
|
+
import { readCache, writeCache } from '../cache.js';
|
|
3
|
+
|
|
4
|
+
const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === '*';
|
|
5
|
+
|
|
6
|
+
const CACHE_KEY = 'agw-status';
|
|
7
|
+
const SUCCESS_TTL = 3000;
|
|
8
|
+
const FAILURE_TTL = 10000;
|
|
9
|
+
|
|
10
|
+
export class AgwProvider implements FrameworkProvider {
|
|
11
|
+
name = 'agw';
|
|
12
|
+
constructor(private endpoint: string, private cacheDir: string) {}
|
|
13
|
+
|
|
14
|
+
isAvailable(): boolean { return true; }
|
|
15
|
+
|
|
16
|
+
async fetch(): Promise<FrameworkStatus | null> {
|
|
17
|
+
const failCached = readCache<boolean>('agw-failure', FAILURE_TTL, this.cacheDir);
|
|
18
|
+
if (failCached === true) return null;
|
|
19
|
+
|
|
20
|
+
const cached = readCache<FrameworkStatus>(CACHE_KEY, SUCCESS_TTL, this.cacheDir);
|
|
21
|
+
if (cached) return cached;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timeout = setTimeout(() => controller.abort(), 200);
|
|
26
|
+
const res = await fetch(`${this.endpoint}/combos`, { signal: controller.signal });
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
|
|
29
|
+
if (!res.ok) { writeCache('agw-failure', true, this.cacheDir); return null; }
|
|
30
|
+
|
|
31
|
+
const combos = await res.json() as Array<{ id: string; status: string; progress?: string }>;
|
|
32
|
+
const entries: FrameworkEntry[] = combos.map(c => ({
|
|
33
|
+
label: c.id,
|
|
34
|
+
status: c.status === 'running' ? 'running' : c.status === 'error' ? 'error' : 'completed',
|
|
35
|
+
progress: c.progress,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const status: FrameworkStatus = { provider: 'AGW', entries };
|
|
39
|
+
writeCache(CACHE_KEY, status, this.cacheDir);
|
|
40
|
+
return status;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (DEBUG) console.error('[claude-hud:agw-provider] connection error:', err);
|
|
43
|
+
writeCache('agw-failure', true, this.cacheDir);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FrameworkProvider, FrameworkStatus } from '../types.js';
|
|
2
|
+
import { AgwProvider } from './agw-provider.js';
|
|
3
|
+
import { AgentTeamsProvider } from './agent-teams-provider.js';
|
|
4
|
+
|
|
5
|
+
interface FrameworksConfig {
|
|
6
|
+
agw: { enabled: boolean; endpoint: string };
|
|
7
|
+
agentTeams: { enabled: boolean };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadProviders(config: FrameworksConfig, cacheDir: string): FrameworkProvider[] {
|
|
11
|
+
const providers: FrameworkProvider[] = [];
|
|
12
|
+
if (config.agw.enabled) providers.push(new AgwProvider(config.agw.endpoint, cacheDir));
|
|
13
|
+
if (config.agentTeams.enabled) providers.push(new AgentTeamsProvider(cacheDir));
|
|
14
|
+
return providers;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function fetchAllProviders(providers: FrameworkProvider[]): Promise<FrameworkStatus[]> {
|
|
18
|
+
const results: FrameworkStatus[] = [];
|
|
19
|
+
for (const provider of providers) {
|
|
20
|
+
if (!provider.isAvailable()) continue;
|
|
21
|
+
try {
|
|
22
|
+
const status = await provider.fetch();
|
|
23
|
+
if (status && status.entries.length > 0) results.push(status);
|
|
24
|
+
} catch { /* Silent skip — error boundary */ }
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RenderContext, AgentEntry } from '../types.js';
|
|
2
|
+
import { yellow, green, magenta, dim } from './colors.js';
|
|
3
|
+
import { truncateString } from '../utils/format.js';
|
|
4
|
+
|
|
5
|
+
export function renderAgentsLine(ctx: RenderContext): string | null {
|
|
6
|
+
const { agents } = ctx.transcript;
|
|
7
|
+
|
|
8
|
+
const runningAgents = agents.filter((a) => a.status === 'running');
|
|
9
|
+
const recentCompleted = agents
|
|
10
|
+
.filter((a) => a.status === 'completed')
|
|
11
|
+
.slice(-2);
|
|
12
|
+
|
|
13
|
+
const toShow = [...runningAgents, ...recentCompleted].slice(-3);
|
|
14
|
+
|
|
15
|
+
if (toShow.length === 0) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const lines: string[] = [];
|
|
20
|
+
|
|
21
|
+
for (const agent of toShow) {
|
|
22
|
+
lines.push(formatAgent(agent));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return lines.join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatAgent(agent: AgentEntry): string {
|
|
29
|
+
const statusIcon = agent.status === 'running' ? yellow('◐') : green('✓');
|
|
30
|
+
const type = magenta(agent.type);
|
|
31
|
+
const model = agent.model ? dim(`[${agent.model}]`) : '';
|
|
32
|
+
const desc = agent.description ? dim(`: ${truncateString(agent.description, 40)}`) : '';
|
|
33
|
+
const elapsed = formatElapsed(agent);
|
|
34
|
+
|
|
35
|
+
return `${statusIcon} ${type}${model ? ` ${model}` : ''}${desc} ${dim(`(${elapsed})`)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
function formatElapsed(agent: AgentEntry): string {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const start = agent.startTime.getTime();
|
|
42
|
+
const end = agent.endTime?.getTime() ?? now;
|
|
43
|
+
const ms = end - start;
|
|
44
|
+
|
|
45
|
+
if (ms < 1000) return '<1s';
|
|
46
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
47
|
+
|
|
48
|
+
const mins = Math.floor(ms / 60000);
|
|
49
|
+
const secs = Math.round((ms % 60000) / 1000);
|
|
50
|
+
return `${mins}m ${secs}s`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Alert } from '../types.js';
|
|
2
|
+
import { red, dim } from './colors.js';
|
|
3
|
+
|
|
4
|
+
export function renderAlertLine(alerts: Alert[]): string | null {
|
|
5
|
+
if (alerts.length === 0) return null;
|
|
6
|
+
const parts = alerts.map(alert => {
|
|
7
|
+
const icon = alert.type.includes('critical') ? '⚠' : '⚡';
|
|
8
|
+
return red(`${icon} ${alert.message}`);
|
|
9
|
+
});
|
|
10
|
+
return parts.join(` ${dim('│')} `);
|
|
11
|
+
}
|