Reorganize: Move all skills to skills/ folder

- Created skills/ directory
- Moved 272 skills to skills/ subfolder
- Kept agents/ at root level
- Kept installation scripts and docs at root level

Repository structure:
- skills/           - All 272 skills from skills.sh
- agents/           - Agent definitions
- *.sh, *.ps1       - Installation scripts
- README.md, etc.   - Documentation

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-23 18:05:17 +00:00
Unverified
parent 2b4e974878
commit b723e2bd7d
4083 changed files with 1056 additions and 1098063 deletions

View File

@@ -0,0 +1,54 @@
import type { RenderContext, AgentEntry } from '../types.js';
import { yellow, green, magenta, dim } from './colors.js';
export function renderAgentsLine(ctx: RenderContext): string | null {
const { agents } = ctx.transcript;
const runningAgents = agents.filter((a) => a.status === 'running');
const recentCompleted = agents
.filter((a) => a.status === 'completed')
.slice(-2);
const toShow = [...runningAgents, ...recentCompleted].slice(-3);
if (toShow.length === 0) {
return null;
}
const lines: string[] = [];
for (const agent of toShow) {
lines.push(formatAgent(agent));
}
return lines.join('\n');
}
function formatAgent(agent: AgentEntry): string {
const statusIcon = agent.status === 'running' ? yellow('◐') : green('✓');
const type = magenta(agent.type);
const model = agent.model ? dim(`[${agent.model}]`) : '';
const desc = agent.description ? dim(`: ${truncateDesc(agent.description)}`) : '';
const elapsed = formatElapsed(agent);
return `${statusIcon} ${type}${model ? ` ${model}` : ''}${desc} ${dim(`(${elapsed})`)}`;
}
function truncateDesc(desc: string, maxLen: number = 40): string {
if (desc.length <= maxLen) return desc;
return desc.slice(0, maxLen - 3) + '...';
}
function formatElapsed(agent: AgentEntry): string {
const now = Date.now();
const start = agent.startTime.getTime();
const end = agent.endTime?.getTime() ?? now;
const ms = end - start;
if (ms < 1000) return '<1s';
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
const mins = Math.floor(ms / 60000);
const secs = Math.round((ms % 60000) / 1000);
return `${mins}m ${secs}s`;
}

View File

@@ -0,0 +1,45 @@
export const RESET = '\x1b[0m';
const DIM = '\x1b[2m';
const RED = '\x1b[31m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const MAGENTA = '\x1b[35m';
const CYAN = '\x1b[36m';
export function green(text: string): string {
return `${GREEN}${text}${RESET}`;
}
export function yellow(text: string): string {
return `${YELLOW}${text}${RESET}`;
}
export function red(text: string): string {
return `${RED}${text}${RESET}`;
}
export function cyan(text: string): string {
return `${CYAN}${text}${RESET}`;
}
export function magenta(text: string): string {
return `${MAGENTA}${text}${RESET}`;
}
export function dim(text: string): string {
return `${DIM}${text}${RESET}`;
}
export function getContextColor(percent: number): string {
if (percent >= 85) return RED;
if (percent >= 70) return YELLOW;
return GREEN;
}
export function coloredBar(percent: number, width: number = 10): string {
const filled = Math.round((percent / 100) * width);
const empty = width - filled;
const color = getContextColor(percent);
return `${color}${'█'.repeat(filled)}${DIM}${'░'.repeat(empty)}${RESET}`;
}

View File

@@ -0,0 +1,111 @@
import type { RenderContext } from '../types.js';
import { renderSessionLine } from './session-line.js';
import { renderToolsLine } from './tools-line.js';
import { renderAgentsLine } from './agents-line.js';
import { renderTodosLine } from './todos-line.js';
import {
renderIdentityLine,
renderProjectLine,
renderEnvironmentLine,
renderUsageLine,
} from './lines/index.js';
import { dim, RESET } from './colors.js';
function visualLength(str: string): number {
// eslint-disable-next-line no-control-regex
return str.replace(/\x1b\[[0-9;]*m/g, '').length;
}
function makeSeparator(length: number): string {
return dim('─'.repeat(Math.max(length, 20)));
}
function collectActivityLines(ctx: RenderContext): string[] {
const activityLines: string[] = [];
const display = ctx.config?.display;
if (display?.showTools !== false) {
const toolsLine = renderToolsLine(ctx);
if (toolsLine) {
activityLines.push(toolsLine);
}
}
if (display?.showAgents !== false) {
const agentsLine = renderAgentsLine(ctx);
if (agentsLine) {
activityLines.push(agentsLine);
}
}
if (display?.showTodos !== false) {
const todosLine = renderTodosLine(ctx);
if (todosLine) {
activityLines.push(todosLine);
}
}
return activityLines;
}
function renderCompact(ctx: RenderContext): string[] {
const lines: string[] = [];
const sessionLine = renderSessionLine(ctx);
if (sessionLine) {
lines.push(sessionLine);
}
return lines;
}
function renderExpanded(ctx: RenderContext): string[] {
const lines: string[] = [];
const identityLine = renderIdentityLine(ctx);
if (identityLine) {
lines.push(identityLine);
}
const projectLine = renderProjectLine(ctx);
if (projectLine) {
lines.push(projectLine);
}
const environmentLine = renderEnvironmentLine(ctx);
if (environmentLine) {
lines.push(environmentLine);
}
const usageLine = renderUsageLine(ctx);
if (usageLine) {
lines.push(usageLine);
}
return lines;
}
export function render(ctx: RenderContext): void {
const lineLayout = ctx.config?.lineLayout ?? 'expanded';
const showSeparators = ctx.config?.showSeparators ?? false;
const headerLines = lineLayout === 'expanded'
? renderExpanded(ctx)
: renderCompact(ctx);
const activityLines = collectActivityLines(ctx);
const lines: string[] = [...headerLines];
if (showSeparators && activityLines.length > 0) {
const maxWidth = Math.max(...headerLines.map(visualLength), 20);
lines.push(makeSeparator(maxWidth));
}
lines.push(...activityLines);
for (const line of lines) {
const outputLine = `${RESET}${line.replace(/ /g, '\u00A0')}`;
console.log(outputLine);
}
}

View File

@@ -0,0 +1,41 @@
import type { RenderContext } from '../../types.js';
import { dim } from '../colors.js';
export function renderEnvironmentLine(ctx: RenderContext): string | null {
const display = ctx.config?.display;
if (display?.showConfigCounts === false) {
return null;
}
const totalCounts = ctx.claudeMdCount + ctx.rulesCount + ctx.mcpCount + ctx.hooksCount;
const threshold = display?.environmentThreshold ?? 0;
if (totalCounts === 0 || totalCounts < threshold) {
return null;
}
const parts: string[] = [];
if (ctx.claudeMdCount > 0) {
parts.push(`${ctx.claudeMdCount} CLAUDE.md`);
}
if (ctx.rulesCount > 0) {
parts.push(`${ctx.rulesCount} rules`);
}
if (ctx.mcpCount > 0) {
parts.push(`${ctx.mcpCount} MCPs`);
}
if (ctx.hooksCount > 0) {
parts.push(`${ctx.hooksCount} hooks`);
}
if (parts.length === 0) {
return null;
}
return dim(parts.join(' | '));
}

View File

@@ -0,0 +1,62 @@
import type { RenderContext } from '../../types.js';
import { getContextPercent, getBufferedPercent, getModelName } from '../../stdin.js';
import { coloredBar, cyan, dim, getContextColor, RESET } from '../colors.js';
const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === '*';
export function renderIdentityLine(ctx: RenderContext): string {
const model = getModelName(ctx.stdin);
const rawPercent = getContextPercent(ctx.stdin);
const bufferedPercent = getBufferedPercent(ctx.stdin);
const autocompactMode = ctx.config?.display?.autocompactBuffer ?? 'enabled';
const percent = autocompactMode === 'disabled' ? rawPercent : bufferedPercent;
if (DEBUG && autocompactMode === 'disabled') {
console.error(`[claude-hud:context] autocompactBuffer=disabled, showing raw ${rawPercent}% (buffered would be ${bufferedPercent}%)`);
}
const bar = coloredBar(percent);
const display = ctx.config?.display;
const parts: string[] = [];
const planName = display?.showUsage !== false ? ctx.usageData?.planName : undefined;
const modelDisplay = planName ? `${model} | ${planName}` : model;
if (display?.showModel !== false && display?.showContextBar !== false) {
parts.push(`${cyan(`[${modelDisplay}]`)} ${bar} ${getContextColor(percent)}${percent}%${RESET}`);
} else if (display?.showModel !== false) {
parts.push(`${cyan(`[${modelDisplay}]`)} ${getContextColor(percent)}${percent}%${RESET}`);
} else if (display?.showContextBar !== false) {
parts.push(`${bar} ${getContextColor(percent)}${percent}%${RESET}`);
} else {
parts.push(`${getContextColor(percent)}${percent}%${RESET}`);
}
if (display?.showDuration !== false && ctx.sessionDuration) {
parts.push(dim(`⏱️ ${ctx.sessionDuration}`));
}
let line = parts.join(' | ');
if (display?.showTokenBreakdown !== false && percent >= 85) {
const usage = ctx.stdin.context_window?.current_usage;
if (usage) {
const input = formatTokens(usage.input_tokens ?? 0);
const cache = formatTokens((usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0));
line += dim(` (in: ${input}, cache: ${cache})`);
}
}
return line;
}
function formatTokens(n: number): string {
if (n >= 1000000) {
return `${(n / 1000000).toFixed(1)}M`;
}
if (n >= 1000) {
return `${(n / 1000).toFixed(0)}k`;
}
return n.toString();
}

View File

@@ -0,0 +1,4 @@
export { renderIdentityLine } from './identity.js';
export { renderProjectLine } from './project.js';
export { renderEnvironmentLine } from './environment.js';
export { renderUsageLine } from './usage.js';

View File

@@ -0,0 +1,49 @@
import type { RenderContext } from '../../types.js';
import { cyan, magenta, yellow } from '../colors.js';
export function renderProjectLine(ctx: RenderContext): string | null {
if (!ctx.stdin.cwd) {
return null;
}
const segments = ctx.stdin.cwd.split(/[/\\]/).filter(Boolean);
const pathLevels = ctx.config?.pathLevels ?? 1;
const projectPath = segments.length > 0 ? segments.slice(-pathLevels).join('/') : '/';
let gitPart = '';
const gitConfig = ctx.config?.gitStatus;
const showGit = gitConfig?.enabled ?? true;
if (showGit && ctx.gitStatus) {
const gitParts: string[] = [ctx.gitStatus.branch];
if ((gitConfig?.showDirty ?? true) && ctx.gitStatus.isDirty) {
gitParts.push('*');
}
if (gitConfig?.showAheadBehind) {
if (ctx.gitStatus.ahead > 0) {
gitParts.push(`${ctx.gitStatus.ahead}`);
}
if (ctx.gitStatus.behind > 0) {
gitParts.push(`${ctx.gitStatus.behind}`);
}
}
if (gitConfig?.showFileStats && ctx.gitStatus.fileStats) {
const { modified, added, deleted, untracked } = ctx.gitStatus.fileStats;
const statParts: string[] = [];
if (modified > 0) statParts.push(`!${modified}`);
if (added > 0) statParts.push(`+${added}`);
if (deleted > 0) statParts.push(`${deleted}`);
if (untracked > 0) statParts.push(`?${untracked}`);
if (statParts.length > 0) {
gitParts.push(` ${statParts.join(' ')}`);
}
}
gitPart = ` ${magenta('git:(')}${cyan(gitParts.join(''))}${magenta(')')}`;
}
return `${yellow(projectPath)}${gitPart}`;
}

View File

@@ -0,0 +1,70 @@
import type { RenderContext } from '../../types.js';
import { isLimitReached } from '../../types.js';
import { red, yellow, dim, getContextColor, RESET } from '../colors.js';
export function renderUsageLine(ctx: RenderContext): string | null {
const display = ctx.config?.display;
if (display?.showUsage === false) {
return null;
}
if (!ctx.usageData?.planName) {
return null;
}
if (ctx.usageData.apiUnavailable) {
return yellow(`usage: ⚠`);
}
if (isLimitReached(ctx.usageData)) {
const resetTime = ctx.usageData.fiveHour === 100
? formatResetTime(ctx.usageData.fiveHourResetAt)
: formatResetTime(ctx.usageData.sevenDayResetAt);
return red(`⚠ Limit reached${resetTime ? ` (resets ${resetTime})` : ''}`);
}
const threshold = display?.usageThreshold ?? 0;
const fiveHour = ctx.usageData.fiveHour;
const sevenDay = ctx.usageData.sevenDay;
const effectiveUsage = Math.max(fiveHour ?? 0, sevenDay ?? 0);
if (effectiveUsage < threshold) {
return null;
}
const fiveHourDisplay = formatUsagePercent(ctx.usageData.fiveHour);
const fiveHourReset = formatResetTime(ctx.usageData.fiveHourResetAt);
const fiveHourPart = fiveHourReset
? `5h: ${fiveHourDisplay} (${fiveHourReset})`
: `5h: ${fiveHourDisplay}`;
if (sevenDay !== null && sevenDay >= 80) {
const sevenDayDisplay = formatUsagePercent(sevenDay);
return `${fiveHourPart} | 7d: ${sevenDayDisplay}`;
}
return fiveHourPart;
}
function formatUsagePercent(percent: number | null): string {
if (percent === null) {
return dim('--');
}
const color = getContextColor(percent);
return `${color}${percent}%${RESET}`;
}
function formatResetTime(resetAt: Date | null): string {
if (!resetAt) return '';
const now = new Date();
const diffMs = resetAt.getTime() - now.getTime();
if (diffMs <= 0) return '';
const diffMins = Math.ceil(diffMs / 60000);
if (diffMins < 60) return `${diffMins}m`;
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
}

View File

@@ -0,0 +1,201 @@
import type { RenderContext } from '../types.js';
import { isLimitReached } from '../types.js';
import { getContextPercent, getBufferedPercent, getModelName } from '../stdin.js';
import { coloredBar, cyan, dim, magenta, red, yellow, getContextColor, RESET } from './colors.js';
const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === '*';
/**
* Renders the full session line (model + context bar + project + git + counts + usage + duration).
* Used for compact layout mode.
*/
export function renderSessionLine(ctx: RenderContext): string {
const model = getModelName(ctx.stdin);
const rawPercent = getContextPercent(ctx.stdin);
const bufferedPercent = getBufferedPercent(ctx.stdin);
const autocompactMode = ctx.config?.display?.autocompactBuffer ?? 'enabled';
const percent = autocompactMode === 'disabled' ? rawPercent : bufferedPercent;
if (DEBUG && autocompactMode === 'disabled') {
console.error(`[claude-hud:context] autocompactBuffer=disabled, showing raw ${rawPercent}% (buffered would be ${bufferedPercent}%)`);
}
const bar = coloredBar(percent);
const parts: string[] = [];
const display = ctx.config?.display;
// Model and context bar (FIRST)
// Plan name only shows if showUsage is enabled (respects hybrid toggle)
const planName = display?.showUsage !== false ? ctx.usageData?.planName : undefined;
const modelDisplay = planName ? `${model} | ${planName}` : model;
if (display?.showModel !== false && display?.showContextBar !== false) {
parts.push(`${cyan(`[${modelDisplay}]`)} ${bar} ${getContextColor(percent)}${percent}%${RESET}`);
} else if (display?.showModel !== false) {
parts.push(`${cyan(`[${modelDisplay}]`)} ${getContextColor(percent)}${percent}%${RESET}`);
} else if (display?.showContextBar !== false) {
parts.push(`${bar} ${getContextColor(percent)}${percent}%${RESET}`);
} else {
parts.push(`${getContextColor(percent)}${percent}%${RESET}`);
}
// Project path (SECOND)
if (ctx.stdin.cwd) {
// Split by both Unix (/) and Windows (\) separators for cross-platform support
const segments = ctx.stdin.cwd.split(/[/\\]/).filter(Boolean);
const pathLevels = ctx.config?.pathLevels ?? 1;
// Always join with forward slash for consistent display
// Handle root path (/) which results in empty segments
const projectPath = segments.length > 0 ? segments.slice(-pathLevels).join('/') : '/';
// Build git status string
let gitPart = '';
const gitConfig = ctx.config?.gitStatus;
const showGit = gitConfig?.enabled ?? true;
if (showGit && ctx.gitStatus) {
const gitParts: string[] = [ctx.gitStatus.branch];
// Show dirty indicator
if ((gitConfig?.showDirty ?? true) && ctx.gitStatus.isDirty) {
gitParts.push('*');
}
// Show ahead/behind (with space separator for readability)
if (gitConfig?.showAheadBehind) {
if (ctx.gitStatus.ahead > 0) {
gitParts.push(`${ctx.gitStatus.ahead}`);
}
if (ctx.gitStatus.behind > 0) {
gitParts.push(`${ctx.gitStatus.behind}`);
}
}
// Show file stats in Starship-compatible format (!modified +added ✘deleted ?untracked)
if (gitConfig?.showFileStats && ctx.gitStatus.fileStats) {
const { modified, added, deleted, untracked } = ctx.gitStatus.fileStats;
const statParts: string[] = [];
if (modified > 0) statParts.push(`!${modified}`);
if (added > 0) statParts.push(`+${added}`);
if (deleted > 0) statParts.push(`${deleted}`);
if (untracked > 0) statParts.push(`?${untracked}`);
if (statParts.length > 0) {
gitParts.push(` ${statParts.join(' ')}`);
}
}
gitPart = ` ${magenta('git:(')}${cyan(gitParts.join(''))}${magenta(')')}`;
}
parts.push(`${yellow(projectPath)}${gitPart}`);
}
// Config counts (respects environmentThreshold)
if (display?.showConfigCounts !== false) {
const totalCounts = ctx.claudeMdCount + ctx.rulesCount + ctx.mcpCount + ctx.hooksCount;
const envThreshold = display?.environmentThreshold ?? 0;
if (totalCounts > 0 && totalCounts >= envThreshold) {
if (ctx.claudeMdCount > 0) {
parts.push(dim(`${ctx.claudeMdCount} CLAUDE.md`));
}
if (ctx.rulesCount > 0) {
parts.push(dim(`${ctx.rulesCount} rules`));
}
if (ctx.mcpCount > 0) {
parts.push(dim(`${ctx.mcpCount} MCPs`));
}
if (ctx.hooksCount > 0) {
parts.push(dim(`${ctx.hooksCount} hooks`));
}
}
}
// Usage limits display (shown when enabled in config, respects usageThreshold)
if (display?.showUsage !== false && ctx.usageData?.planName) {
if (ctx.usageData.apiUnavailable) {
parts.push(yellow(`usage: ⚠`));
} else if (isLimitReached(ctx.usageData)) {
const resetTime = ctx.usageData.fiveHour === 100
? formatResetTime(ctx.usageData.fiveHourResetAt)
: formatResetTime(ctx.usageData.sevenDayResetAt);
parts.push(red(`⚠ Limit reached${resetTime ? ` (resets ${resetTime})` : ''}`));
} else {
const usageThreshold = display?.usageThreshold ?? 0;
const fiveHour = ctx.usageData.fiveHour;
const sevenDay = ctx.usageData.sevenDay;
const effectiveUsage = Math.max(fiveHour ?? 0, sevenDay ?? 0);
if (effectiveUsage >= usageThreshold) {
const fiveHourDisplay = formatUsagePercent(fiveHour);
const fiveHourReset = formatResetTime(ctx.usageData.fiveHourResetAt);
const fiveHourPart = fiveHourReset
? `5h: ${fiveHourDisplay} (${fiveHourReset})`
: `5h: ${fiveHourDisplay}`;
if (sevenDay !== null && sevenDay >= 80) {
const sevenDayDisplay = formatUsagePercent(sevenDay);
parts.push(`${fiveHourPart} | 7d: ${sevenDayDisplay}`);
} else {
parts.push(fiveHourPart);
}
}
}
}
// Session duration
if (display?.showDuration !== false && ctx.sessionDuration) {
parts.push(dim(`⏱️ ${ctx.sessionDuration}`));
}
let line = parts.join(' | ');
// Token breakdown at high context
if (display?.showTokenBreakdown !== false && percent >= 85) {
const usage = ctx.stdin.context_window?.current_usage;
if (usage) {
const input = formatTokens(usage.input_tokens ?? 0);
const cache = formatTokens((usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0));
line += dim(` (in: ${input}, cache: ${cache})`);
}
}
return line;
}
function formatTokens(n: number): string {
if (n >= 1000000) {
return `${(n / 1000000).toFixed(1)}M`;
}
if (n >= 1000) {
return `${(n / 1000).toFixed(0)}k`;
}
return n.toString();
}
function formatUsagePercent(percent: number | null): string {
if (percent === null) {
return dim('--');
}
const color = getContextColor(percent);
return `${color}${percent}%${RESET}`;
}
function formatResetTime(resetAt: Date | null): string {
if (!resetAt) return '';
const now = new Date();
const diffMs = resetAt.getTime() - now.getTime();
if (diffMs <= 0) return '';
const diffMins = Math.ceil(diffMs / 60000);
if (diffMins < 60) return `${diffMins}m`;
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
}

View File

@@ -0,0 +1,31 @@
import type { RenderContext } from '../types.js';
import { yellow, green, dim } from './colors.js';
export function renderTodosLine(ctx: RenderContext): string | null {
const { todos } = ctx.transcript;
if (!todos || todos.length === 0) {
return null;
}
const inProgress = todos.find((t) => t.status === 'in_progress');
const completed = todos.filter((t) => t.status === 'completed').length;
const total = todos.length;
if (!inProgress) {
if (completed === total && total > 0) {
return `${green('✓')} All todos complete ${dim(`(${completed}/${total})`)}`;
}
return null;
}
const content = truncateContent(inProgress.content);
const progress = dim(`(${completed}/${total})`);
return `${yellow('▸')} ${content} ${progress}`;
}
function truncateContent(content: string, maxLen: number = 50): string {
if (content.length <= maxLen) return content;
return content.slice(0, maxLen - 3) + '...';
}

View File

@@ -0,0 +1,57 @@
import type { RenderContext } from '../types.js';
import { yellow, green, cyan, dim } from './colors.js';
export function renderToolsLine(ctx: RenderContext): string | null {
const { tools } = ctx.transcript;
if (tools.length === 0) {
return null;
}
const parts: string[] = [];
const runningTools = tools.filter((t) => t.status === 'running');
const completedTools = tools.filter((t) => t.status === 'completed' || t.status === 'error');
for (const tool of runningTools.slice(-2)) {
const target = tool.target ? truncatePath(tool.target) : '';
parts.push(`${yellow('◐')} ${cyan(tool.name)}${target ? dim(`: ${target}`) : ''}`);
}
const toolCounts = new Map<string, number>();
for (const tool of completedTools) {
const count = toolCounts.get(tool.name) ?? 0;
toolCounts.set(tool.name, count + 1);
}
const sortedTools = Array.from(toolCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 4);
for (const [name, count] of sortedTools) {
parts.push(`${green('✓')} ${name} ${dim(`×${count}`)}`);
}
if (parts.length === 0) {
return null;
}
return parts.join(' | ');
}
function truncatePath(path: string, maxLen: number = 20): string {
// Normalize Windows backslashes to forward slashes for consistent display
const normalizedPath = path.replace(/\\/g, '/');
if (normalizedPath.length <= maxLen) return normalizedPath;
// Split by forward slash (already normalized)
const parts = normalizedPath.split('/');
const filename = parts.pop() || normalizedPath;
if (filename.length >= maxLen) {
return filename.slice(0, maxLen - 3) + '...';
}
return '.../' + filename;
}