#!/usr/bin/env node // CC Status Board — Installer for Claude Code // Installs: statusline + asset suggest hook + index builder
const fs = require('fs'); const path = require('path'); const os = require('os');
const HOME = os.homedir(); const CLAUDE_DIR = path.join(HOME, '.claude'); const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks'); const CACHE_DIR = path.join(CLAUDE_DIR, 'cache'); const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json'); const SRC_DIR = path.join(__dirname, 'src');
const GREEN = '\x1b[32m'; const CYAN = '\x1b[36m'; const YELLOW = '\x1b[33m'; const DIM = '\x1b[2m'; const RESET = '\x1b[0m';
console.log(${CYAN}╔══════════════════════════════════════════════╗ ║ CC Status Board v1.0.0 ║ ║ Smart status bar for Claude Code ║ ╚══════════════════════════════════════════════╝${RESET});
// Ensure directories exist for (const dir of [CLAUDE_DIR, HOOKS_DIR, CACHE_DIR]) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); }
// 1. Install hooks const files = { 'cc-statusline.js': 'statusline.js', 'cc-asset-suggest.js': 'asset-suggest.js', 'cc-build-index.js': 'build-index.js', };
for (const [target, source] of Object.entries(files)) {
const src = path.join(SRC_DIR, source);
const dst = path.join(HOOKS_DIR, target);
fs.copyFileSync(src, dst);
console.log( ${GREEN}✓${RESET} Installed ${CYAN}${target}${RESET} -> hooks/);
}
// 2. Update settings.json let settings = {}; if (fs.existsSync(SETTINGS_PATH)) { try { settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8')); } catch (e) {} }
// Statusline
const statuslineCmd = node "${path.join(HOOKS_DIR, 'cc-statusline.js')}";
if (!settings.statusLine || !settings.statusLine.command?.includes('cc-statusline')) {
// Don't overwrite if user has a custom statusline, just inform
if (settings.statusLine && !settings.statusLine.command?.includes('gsd-statusline') && !settings.statusLine.command?.includes('cc-statusline')) {
console.log( ${YELLOW}⚠${RESET} Existing statusline detected, not overwriting.);
console.log( ${DIM}To use CC Status Board's statusline, set manually:${RESET});
console.log( ${DIM}"statusLine": {"type":"command","command":"${statuslineCmd}"}${RESET});
} else {
settings.statusLine = { type: 'command', command: statuslineCmd };
console.log( ${GREEN}✓${RESET} Configured statusline);
}
}
// UserPromptSubmit hook if (!settings.hooks) settings.hooks = {}; if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
const hookCmd = node "${path.join(HOOKS_DIR, 'cc-asset-suggest.js')}";
const hasAssetHook = settings.hooks.UserPromptSubmit.some(h =>
h.hooks?.some(hh => hh.command?.includes('cc-asset-suggest') || hh.command?.includes('asset-suggest'))
);
if (!hasAssetHook) {
settings.hooks.UserPromptSubmit.push({
hooks: [{ type: 'command', command: hookCmd, timeout: 3 }]
});
console.log( ${GREEN}✓${RESET} Configured asset suggest hook);
} else {
console.log( ${DIM} Asset suggest hook already configured${RESET});
}
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
console.log( ${GREEN}✓${RESET} Updated settings.json);
// 3. Build initial asset index
console.log(\n Building asset index...);
try {
const { execSync } = require('child_process');
execSync(node "${path.join(HOOKS_DIR, 'cc-build-index.js')}", { stdio: 'inherit', timeout: 120000 });
} catch (e) {
console.log( ${YELLOW}⚠${RESET} Index build failed: ${e.message});
console.log( ${DIM}Run manually: node ~/.claude/hooks/cc-build-index.js${RESET});
}
console.log(` ${GREEN}Done!${RESET} Restart Claude Code to see:
${CYAN}Opus 4.6 (1M context) │ myproject █░░░░░░░░░ 12% │ 💡 /pdf /xlsx${RESET}
${DIM}• Context meter — see how much context window you've used • Asset suggest — relevant AI assets appear as you type • Rebuild index: node ~/.claude/hooks/cc-build-index.js${RESET} `);
#!/usr/bin/env node // Build a searchable index of ALL installed AI assets // Sources: skills, agents, commands, plugins, MCP servers, hooks // Output: ~/.claude/cache/asset-index.json // // v2: Intent-based classification + TF-IDF weights
const fs = require('fs'); const path = require('path'); const os = require('os');
const HOME = os.homedir(); const CLAUDE_DIR = path.join(HOME, '.claude'); const OUTPUT = path.join(CLAUDE_DIR, 'cache', 'asset-index.json');
const cacheDir = path.join(CLAUDE_DIR, 'cache'); if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
// ── Intent taxonomy ────────────────────────────────────────────── // Each intent has trigger patterns (checked against name+desc) and // query aliases (what users might type to mean this intent). const INTENT_TAXONOMY = { 'seo': { triggers: ['seo', 'search engine', 'keyword research', 'on-page', 'meta tag', 'sitemap', 'indexing', 'serp'], aliases: ['seo', '搜索引擎', '关键词', '排名', 'ranking', 'keyword', 'serp', 'sitemap', '收录'] }, 'backlink': { triggers: ['backlink', 'link building', 'link-building', 'outreach', 'guest post', 'external link'], aliases: ['backlink', '外链', 'link building', '反链', 'outreach', '链接建设'] }, 'content-marketing': { triggers: ['content creation', 'content marketing', 'blog', 'newsletter', 'copywriting', 'draft content', 'draft-content'], aliases: ['content', '内容', 'blog', 'copywriting', '文案', 'newsletter', '写作'] }, 'competitive-analysis': { triggers: ['competitor', 'competitive', 'market positioning', 'battlecard', 'competitive-'], aliases: ['competitor', '竞品', '竞争', 'competitive', '对手', 'battlecard', '竞分'] }, 'scraping': { triggers: ['scrape', 'scraping', 'crawl', 'extract data', 'web scraper', 'spider'], aliases: ['scrape', '爬虫', 'crawl', '抓取', '采集', 'spider', 'extract'] }, 'pdf': { triggers: ['pdf', '.pdf'], aliases: ['pdf'] }, 'spreadsheet': { triggers: ['xlsx', 'excel', 'spreadsheet', '.csv', 'tsv', 'tabular'], aliases: ['excel', 'xlsx', '表格', 'spreadsheet', 'csv'] }, 'slides': { triggers: ['pptx', 'slide', 'presentation', 'deck', 'pitch'], aliases: ['ppt', 'pptx', '幻灯片', 'slide', 'presentation', 'deck', '演示'] }, 'document': { triggers: ['docx', 'word document', '.docx'], aliases: ['docx', 'word', '文档'] }, 'frontend': { triggers: ['frontend', 'front-end', 'ui design', 'web component', 'react', 'css', 'html', 'landing page', 'dashboard'], aliases: ['frontend', '前端', 'ui', 'design', '页面', 'component', 'landing', 'dashboard', '界面'] }, 'database': { triggers: ['database', 'mysql', 'sql', 'query', 'postgresql', 'sqlite', 'migration', 'schema'], aliases: ['database', '数据库', 'sql', 'mysql', 'query', '查询', 'db'] }, 'deploy': { triggers: ['deploy', 'deployment', 'production', 'ci/cd', 'pm2', 'server', 'hosting'], aliases: ['deploy', '部署', 'production', '上线', '发布', 'server', '服务器'] }, 'debug': { triggers: ['debug', 'debugging', 'troubleshoot', 'diagnose', 'error', 'bug fix'], aliases: ['debug', '调试', 'bug', '报错', 'error', 'fix', '修复', 'troubleshoot'] }, 'testing': { triggers: ['test', 'testing', 'qa', 'unit test', 'integration test', 'e2e', 'playwright'], aliases: ['test', '测试', 'qa', 'unittest', 'e2e', '自动化测试'] }, 'code-review': { triggers: ['code review', 'code-review', 'review code', 'pull request review', 'adversarial review'], aliases: ['review', '代码审查', 'code review', 'pr review'] }, 'video': { triggers: ['video', 'youtube', 'tiktok', 'remotion', 'gif', 'animation'], aliases: ['video', '视频', 'youtube', 'tiktok', '短视频', 'gif', '动画'] }, 'email': { triggers: ['email', 'mail', 'outreach', 'newsletter', 'email sequence', 'drip'], aliases: ['email', '邮件', 'mail', 'outreach', 'newsletter'] }, 'sales': { triggers: ['sales', 'pipeline', 'forecast', 'deal', 'prospect', 'crm', 'call prep'], aliases: ['sales', '销售', 'pipeline', 'forecast', 'prospect', 'crm', '客户'] }, 'project-management': { triggers: ['project', 'roadmap', 'sprint', 'milestone', 'epic', 'story', 'kanban', 'scrum'], aliases: ['project', '项目', 'roadmap', 'sprint', '规划', 'milestone', '里程碑'] }, 'planning': { triggers: ['plan', 'planning', 'architect', 'design doc', 'spec', 'prd', 'requirement'], aliases: ['plan', '计划', '规划', 'spec', 'prd', '需求', 'architecture', '架构'] }, 'research': { triggers: ['research', 'investigate', 'discovery', 'literature', 'study', 'explore'], aliases: ['research', '研究', '调研', 'investigate', 'explore', '探索'] }, 'analytics': { triggers: ['analytics', 'metrics', 'data analysis', 'statistics', 'performance report', 'kpi'], aliases: ['analytics', '分析', 'metrics', '数据', 'kpi', '报表', 'data', '统计'] }, 'social-media': { triggers: ['social media', 'twitter', 'instagram', 'facebook', 'tiktok', 'influencer', 'brand'], aliases: ['social', '社交', '社媒', 'twitter', 'instagram', 'tiktok', '小红书', '抖音'] }, 'automation': { triggers: ['automat', 'workflow', 'n8n', 'zapier', 'script', 'cron', 'schedule'], aliases: ['automation', '自动化', 'workflow', 'script', '脚本', 'cron', '定时'] }, 'browser': { triggers: ['chrome', 'browser', 'playwright', 'puppeteer', 'selenium', 'web automation'], aliases: ['chrome', '浏览器', 'browser', 'playwright', '网页自动化'] }, 'mcp-server': { triggers: ['mcp', 'model context protocol', 'mcp server', 'mcp-'], aliases: ['mcp', '协议', 'server', 'integration'] }, 'ai-agent': { triggers: ['agent', 'multi-agent', 'autonomous', 'crew', 'langgraph', 'autogen'], aliases: ['agent', '智能体', 'autonomous', '多agent', 'crew'] }, 'prompt-engineering': { triggers: ['prompt', 'system prompt', 'instruction', 'cursorrule', '.cursorrules'], aliases: ['prompt', '提示词', 'system prompt', 'instruction'] }, 'finance': { triggers: ['finance', 'accounting', 'journal entry', 'reconciliation', 'sox', 'income statement', 'variance'], aliases: ['finance', '财务', '会计', 'accounting', 'journal', '对账'] }, 'legal': { triggers: ['legal', 'contract', 'nda', 'compliance', 'gdpr', 'privacy'], aliases: ['legal', '法务', 'contract', '合同', 'nda', 'compliance', '合规'] }, 'customer-support': { triggers: ['support', 'ticket', 'triage', 'escalat', 'knowledge base', 'kb article'], aliases: ['support', '客服', 'ticket', '工单', 'triage', 'escalation'] }, 'product-management': { triggers: ['product', 'prd', 'feature spec', 'roadmap', 'stakeholder', 'user research'], aliases: ['product', '产品', 'prd', 'feature', 'roadmap', 'stakeholder'] }, 'documentation': { triggers: ['documentation', 'docs', 'technical writing', 'readme', 'api doc'], aliases: ['docs', '文档', 'documentation', 'readme', '写文档'] }, 'bio-research': { triggers: ['bio', 'clinical', 'preprint', 'chembl', 'drug', 'genomic', 'single-cell', 'rna'], aliases: ['bio', '生物', 'clinical', '临床', 'drug', '药物', 'genomic'] }, 'trading': { triggers: ['trade', 'trading', 'prediction', 'market', 'forecast', 'macro'], aliases: ['trade', '交易', 'trading', 'market', '预测', 'macro', '宏观'] }, 'health': { triggers: ['body', 'health', 'track', 'fitness', 'weight', 'diet'], aliases: ['health', '健康', 'body', '身体', 'fitness', '体重', '饮食'] }, 'art-design': { triggers: ['art', 'design', 'creative', 'visual', 'illustration', 'canvas', 'algorithmic'], aliases: ['art', '艺术', 'design', '设计', 'creative', '创意', 'visual'] }, 'promotion': { triggers: ['promot', 'marketing', 'growth', 'lead gen', 'campaign', 'nurture'], aliases: ['promote', '推广', 'marketing', '营销', 'growth', 'campaign', '活动'] }, 'security': { triggers: ['security', 'secure', 'threat', 'vulnerability', 'pentest'], aliases: ['security', '安全', 'secure', 'threat', '漏洞'] } };
function extractFrontmatter(content) { const m = content.match(/^---\n([\s\S]?)\n---/); if (!m) return {}; const fm = m[1]; const name = fm.match(/^name:\s["']?(.+?)["']?\s*$/m); const desc = fm.match(/^description:\s*["']?([\s\S]?)["']?\s$/m); let d = desc ? desc[1].trim().replace(/["']$/, '').trim() : ''; if (d.length > 300) d = d.substring(0, 300); return { name: name ? name[1].trim() : null, desc: d }; }
// Classify asset into intents
function classifyIntents(name, desc) {
const text = ${name || ''} ${desc || ''}.toLowerCase();
const intents = [];
for (const [intent, { triggers }] of Object.entries(INTENT_TAXONOMY)) {
for (const t of triggers) {
if (text.includes(t)) {
intents.push(intent);
break;
}
}
}
return intents.length > 0 ? intents : ['general'];
}
function buildKeywords(name, desc) { const stop = new Set(['a','an','the','is','are','was','were','be','been','being', 'have','has','had','do','does','did','will','would','shall','should','may', 'might','must','can','could','this','that','these','those','it','its', 'for','and','or','but','in','on','at','to','of','with','by','from','as', 'into','through','during','before','after','above','below','between', 'use','when','user','wants','any','all','time','also','not','do','if', 'about','using','used','such','like','including','includes','your','you', 'they','them','their','more','most','other','than','then','just','only', 'very','too','so','up','out','no','each','every','both','few','own','same', 'how','what','which','who','where','why','need','needs','based','via', 'tool','tools','skill','command','server','create','run','trigger', 'should','provide','provides','available','specific','type']);
const text = ${name || ''} ${desc || ''}.toLowerCase();
const words = text.match(/[a-z][a-z0-9-]*[a-z0-9]/g) || [];
return [...new Set(words.filter(w => !stop.has(w) && w.length >= 3))];
}
// ── Bilingual keyword expansion via Claude Code CLI ─────────────
// Uses claude -p (print mode) to generate Chinese keywords for
// English assets. Zero config — every CC user has the CLI available.
const { execSync } = require('child_process');
function findClaudeCLI() { // execSync doesn't see shell aliases, so resolve the real binary path const candidates = [ path.join(HOME, '.npm-global/bin/claude'), '/usr/local/bin/claude', '/opt/homebrew/bin/claude', ]; for (const p of candidates) { if (fs.existsSync(p)) return p; } // Fallback: ask shell to resolve try { return execSync('command -v claude', { encoding: 'utf8', stdio: ['pipe','pipe','ignore'] }).trim(); } catch { return null; } }
function expandBilingualKeywords(assets) { if (process.env.CC_SKIP_BILINGUAL === '1') return;
const claudeBin = findClaudeCLI(); if (!claudeBin) { console.log(' \x1b[33m⚠\x1b[0m claude CLI not found. Skipping bilingual keywords.'); return; }
const nonHook = assets.filter(a => a.type !== 'hook'); if (nonHook.length === 0) return;
console.log( Generating bilingual keywords for ${nonHook.length} assets via Claude CLI...);
const BATCH = 40; let expanded = 0;
for (let i = 0; i < nonHook.length; i += BATCH) {
const batch = nonHook.slice(i, i + BATCH);
const toolList = batch.map((a, idx) =>
${idx + 1}. ${a.name}: ${(a.desc || '').substring(0, 100)}
).join('\n');
const prompt = `For each AI tool below, generate 3-8 Chinese search keywords a Chinese-speaking developer would type to find it. Output ONLY a JSON object mapping the exact tool name to an array of Chinese keywords. No markdown, no explanation, no code fences.Tools: ${toolList}`;
try {
const raw = execSync(
`${JSON.stringify(claudeBin)} -p --model haiku`,
{ input: prompt, encoding: 'utf8', timeout: 90000, stdio: ['pipe', 'pipe', 'ignore'] }
);
const jsonMatch = raw.match(/\{[\s\S]*\}/);
if (!jsonMatch) continue;
const zhMap = JSON.parse(jsonMatch[0]);
for (const asset of batch) {
const zhList = zhMap[asset.name];
if (!Array.isArray(zhList)) continue;
const extra = [];
for (const kw of zhList) {
const lower = kw.toLowerCase();
extra.push(lower);
// Generate CJK bigrams so asset-suggest tokenizer can match
const cjk = lower.match(/[\u4e00-\u9fff]+/g) || [];
for (const seg of cjk) {
for (let j = 0; j < seg.length - 1; j++) extra.push(seg.slice(j, j + 2));
}
}
asset.keywords = [...new Set([...asset.keywords, ...extra])];
expanded++;
}
console.log(` \x1b[32m✓\x1b[0m Bilingual keywords: batch ${Math.floor(i / BATCH) + 1} (${batch.length} assets)`);
} catch (e) {
console.log(` \x1b[33m⚠\x1b[0m Claude CLI call failed for batch ${Math.floor(i / BATCH) + 1}: ${e.message.split('\n')[0]}`);
}}
if (expanded > 0) {
console.log( \x1b[32m✓\x1b[0m Bilingual keywords added for ${expanded}/${nonHook.length} assets);
}
}
// ── Main ──────────────────────────────────────────────────────── function main() {
const assets = [];
// ── Scan all asset sources ───────────────────────────────────────
// 1. Skills
const skillsDir = path.join(CLAUDE_DIR, 'skills');
if (fs.existsSync(skillsDir)) {
for (const d of fs.readdirSync(skillsDir)) {
const fp = path.join(skillsDir, d, 'SKILL.md');
if (!fs.existsSync(fp)) continue;
const { name, desc } = extractFrontmatter(fs.readFileSync(fp, 'utf8'));
if (!name) continue;
assets.push({ type: 'skill', name, invoke: /${name}, desc,
intents: classifyIntents(name, desc), keywords: buildKeywords(name, desc) });
}
}
// 2. Agents
const agentsDir = path.join(CLAUDE_DIR, 'agents');
if (fs.existsSync(agentsDir)) {
for (const f of fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'))) {
const { name, desc } = extractFrontmatter(fs.readFileSync(path.join(agentsDir, f), 'utf8'));
if (!name) continue;
assets.push({ type: 'agent', name, invoke: Agent("${name}"), desc,
intents: classifyIntents(name, desc), keywords: buildKeywords(name, desc) });
}
}
// 3. Commands
const cmdsDir = path.join(CLAUDE_DIR, 'commands');
if (fs.existsSync(cmdsDir)) {
for (const f of fs.readdirSync(cmdsDir).filter(f => f.endsWith('.md'))) {
const { name, desc } = extractFrontmatter(fs.readFileSync(path.join(cmdsDir, f), 'utf8'));
if (!name) continue;
assets.push({ type: 'command', name, invoke: /${name}, desc,
intents: classifyIntents(name, desc), keywords: buildKeywords(name, desc) });
}
}
// 4. Plugin Skills & Commands
const pluginsDir = path.join(CLAUDE_DIR, 'plugins');
if (fs.existsSync(pluginsDir)) {
for (const sub of ['cache', 'marketplaces']) {
const baseDir = path.join(pluginsDir, sub);
if (!fs.existsSync(baseDir)) continue;
const walk = (dir, depth) => {
if (depth > 6) return;
try {
for (const f of fs.readdirSync(dir)) {
const fp = path.join(dir, f);
if (fs.statSync(fp).isDirectory()) { walk(fp, depth + 1); continue; }
if (!f.endsWith('.md') || !(dir.includes('/commands') || dir.includes('/skills/'))) continue;
const isSkillFile = f === 'SKILL.md';
let { name: cmdName, desc } = extractFrontmatter(fs.readFileSync(fp, 'utf8'));
if (!cmdName) cmdName = isSkillFile ? path.basename(path.dirname(fp)) : f.replace(/.md$/, '');
if (!cmdName || cmdName === 'commands' || cmdName === 'skills') continue;
const parts = fp.split('/');
let pluginName = 'plugin';
const anchor = isSkillFile ? 'skills' : 'commands';
const anchorIdx = parts.lastIndexOf(anchor);
if (anchorIdx >= 1) {
for (let i = anchorIdx - 1; i >= 0; i--) {
if (!/^\d+.\d+/.test(parts[i]) && parts[i] !== sub) { pluginName = parts[i]; break; }
}
}
const fullName = ${pluginName}:${cmdName};
if (assets.some(a => a.name === fullName || a.name === cmdName)) continue;
assets.push({ type: 'plugin', name: fullName, invoke: /${fullName}, desc: desc || '',
intents: classifyIntents(fullName, desc), keywords: buildKeywords(fullName, desc) });
}
} catch (e) {}
};
walk(baseDir, 0);
}
}
// 5. MCP Servers
for (const mcpPath of [path.join(HOME, '.mcp.json'), path.join(CLAUDE_DIR, '.mcp.json')]) {
if (!fs.existsSync(mcpPath)) continue;
try {
const servers = JSON.parse(fs.readFileSync(mcpPath, 'utf8')).mcpServers || {};
for (const [name, config] of Object.entries(servers)) {
const desc = MCP: ${name} (${config.command || ''});
assets.push({ type: 'mcp', name, invoke: mcp__${name}__*, desc,
intents: classifyIntents(name, desc), keywords: buildKeywords(name, desc) });
}
} catch (e) {}
}
// 6. Hooks (informational, not suggested)
const hooksDir = path.join(CLAUDE_DIR, 'hooks');
if (fs.existsSync(hooksDir)) {
for (const f of fs.readdirSync(hooksDir).filter(f => f.endsWith('.js') || f.endsWith('.sh'))) {
const name = f.replace(/.(js|sh)$/, '');
assets.push({ type: 'hook', name, invoke: '(auto)', desc: Hook: ${name},
intents: ['general'], keywords: [name] });
}
}
// ── Bilingual expansion (Claude CLI) ──────────────────────────── expandBilingualKeywords(assets);
// ── Compute IDF weights ────────────────────────────────────────── const docCount = assets.length; const df = {}; // document frequency per keyword for (const a of assets) { const unique = new Set(a.keywords); for (const kw of unique) { df[kw] = (df[kw] || 0) + 1; } } const idf = {}; for (const [kw, freq] of Object.entries(df)) { idf[kw] = Math.log(docCount / freq); }
// ── Write index ────────────────────────────────────────────────── const index = { version: 2, built_at: new Date().toISOString(), total: assets.length, intent_taxonomy: Object.fromEntries( Object.entries(INTENT_TAXONOMY).map(([k, v]) => [k, v.aliases]) ), idf, assets };
fs.writeFileSync(OUTPUT, JSON.stringify(index));
const types = {};
assets.forEach(a => { types[a.type] = (types[a.type] || 0) + 1; });
console.log(Asset index v2 built: ${assets.length} assets -> ${OUTPUT});
for (const [t, c] of Object.entries(types)) console.log( ${t}: ${c});
} // end main
main();
#!/usr/bin/env node // Asset Suggest Hook v3 (UserPromptSubmit) // Intent matching + TF-IDF + specificity bonus + full-text search // Writes top 3 matches to bridge file for statusline display
const fs = require('fs'); const path = require('path'); const os = require('os');
const INDEX_PATH = path.join(os.homedir(), '.claude/cache/asset-index.json'); const BRIDGE_PATH = path.join(os.tmpdir(), 'claude-asset-suggest.json'); const MAX_SUGGESTIONS = 3; const MIN_SCORE = 5;
let input = ''; const timeout = setTimeout(() => process.exit(0), 3000); process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => input += chunk); process.stdin.on('end', () => { clearTimeout(timeout); try { const data = JSON.parse(input); const userMessage = (data.message || data.prompt || '').toLowerCase();
if (userMessage.length < 3 || userMessage.startsWith('/')) process.exit(0);
if (!fs.existsSync(INDEX_PATH)) process.exit(0);
const index = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'));
if (index.version !== 2) process.exit(0);
// ── Step 1: Tokenize ─────────────────────────────────────────
const latinWords = userMessage.match(/[a-z][a-z0-9-]*[a-z0-9]/g) || [];
const cjkChars = userMessage.match(/[\u4e00-\u9fff]+/g) || [];
const cjkTokens = [];
for (const seg of cjkChars) {
cjkTokens.push(seg);
for (let i = 0; i < seg.length - 1; i++) cjkTokens.push(seg.slice(i, i + 2));
if (seg.length >= 3) {
for (let i = 0; i < seg.length - 2; i++) cjkTokens.push(seg.slice(i, i + 3));
}
}
const queryTokens = new Set([...latinWords, ...cjkTokens]);
// ── Step 2: Detect user intents ──────────────────────────────
const userIntents = new Set();
const intentAliases = index.intent_taxonomy || {};
for (const [intent, aliases] of Object.entries(intentAliases)) {
for (const alias of aliases) {
if (queryTokens.has(alias) || userMessage.includes(alias)) {
userIntents.add(intent);
break;
}
}
}
// ── Step 3: Score each asset ─────────────────────────────────
const idf = index.idf || {};
const scored = index.assets
.filter(a => a.type !== 'hook')
.map(asset => {
let score = 0;
const assetIntents = asset.intents || [];
const intentCount = assetIntents.length || 1;
// A) Intent match with SPECIFICITY bonus
// An asset with 2 intents matching 1 = 10 * (1 / sqrt(2)) = 7.07
// An asset with 8 intents matching 1 = 10 * (1 / sqrt(8)) = 3.54
// Specialist assets rank higher than generalists
let intentOverlap = 0;
for (const ui of userIntents) {
if (assetIntents.includes(ui)) intentOverlap++;
}
if (intentOverlap > 0) {
const specificityMultiplier = 1 / Math.sqrt(intentCount);
score += intentOverlap * 10 * specificityMultiplier;
// Extra bonus for multiple intent matches (rare, high precision)
if (intentOverlap >= 2) score += intentOverlap * 5;
}
// B) TF-IDF keyword match
const assetKwSet = new Set(asset.keywords || []);
for (const qt of queryTokens) {
if (assetKwSet.has(qt)) {
score += (idf[qt] || 1) * 1.5;
}
}
// C) Direct name match — strongest precision signal
const nameLower = asset.name.toLowerCase().replace(/[:-]/g, ' ');
const nameParts = nameLower.split(/\s+/);
for (const qt of queryTokens) {
if (qt.length < 3) continue;
// Exact part match (e.g., "seo" matches "seo-audit")
if (nameParts.some(p => p === qt)) {
score += 12;
} else if (nameLower.includes(qt)) {
score += 6;
}
}
// D) Full-text description search
const descLower = (asset.desc || '').toLowerCase();
for (const qt of queryTokens) {
if (qt.length < 3) continue;
// Count occurrences for density signal
const regex = new RegExp(qt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
const matches = descLower.match(regex);
if (matches) {
score += Math.min(matches.length * 0.8, 3); // cap at 3
}
}
// E) Penalize assets with no intent overlap when user has clear intents
if (userIntents.size > 0 && intentOverlap === 0) {
score *= 0.2;
}
// F) Dedup penalty: if asset type is 'plugin' and a skill/command
// with same base name exists, slightly prefer the non-plugin
if (asset.type === 'plugin') {
score *= 0.95;
}
return { name: asset.name, type: asset.type, invoke: asset.invoke,
desc: asset.desc, score };
})
.filter(a => a.score >= MIN_SCORE)
.sort((a, b) => b.score - a.score);
// ── Step 4: Diversity filter — no more than 2 from same namespace ──
const result = [];
const nsCounts = {};
for (const a of scored) {
const ns = a.name.split(':')[0] || a.name.split('-')[0];
nsCounts[ns] = (nsCounts[ns] || 0) + 1;
if (nsCounts[ns] <= 2) result.push(a);
if (result.length >= MAX_SUGGESTIONS) break;
}
if (result.length === 0) {
try { fs.unlinkSync(BRIDGE_PATH); } catch (e) {}
process.exit(0);
}
fs.writeFileSync(BRIDGE_PATH, JSON.stringify({
ts: Date.now(),
top: result.map(a => ({
type: a.type, name: a.name, invoke: a.invoke,
desc: (a.desc || '').length > 60 ? (a.desc || '').substring(0, 57) + '...' : (a.desc || ''),
score: Math.round(a.score * 10) / 10
}))
}));} catch (e) { process.exit(0); } });
#!/usr/bin/env node // CC Status Board — Statusline // Shows: model │ directory │ context bar │ asset suggestions
const fs = require('fs'); const path = require('path'); const os = require('os');
let input = ''; const stdinTimeout = setTimeout(() => process.exit(0), 3000); process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => input += chunk); process.stdin.on('end', () => { clearTimeout(stdinTimeout); try { const data = JSON.parse(input); const model = data.model?.display_name || 'Claude'; const dir = data.workspace?.current_dir || process.cwd(); const remaining = data.context_window?.remaining_percentage; const ctxSize = data.context_window?.context_window_size;
// ── Context meter ────────────────────────────────────────────
// Claude Code reserves ~16.5% for autocompact buffer.
// We normalize so 100% = point where autocompact triggers.
const AUTO_COMPACT_BUFFER_PCT = 16.5;
let ctx = '';
if (remaining != null) {
const usableRemaining = Math.max(0,
((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
const filled = Math.floor(used / 10);
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled);
// Smart tips based on context usage
let tip = '';
if (used >= 90) {
tip = ' \x1b[5;31m⚠ /compact now or start new chat\x1b[0m';
} else if (used >= 75) {
tip = ' \x1b[38;5;208m→ /compact recommended\x1b[0m';
} else if (used >= 60) {
tip = ' \x1b[33m→ wrap up or /compact soon\x1b[0m';
} else if (used >= 40) {
tip = ' \x1b[2m→ /commit before context fills\x1b[0m';
}
if (used < 50) {
ctx = ` \x1b[32m${bar} ${used}%\x1b[0m${tip}`;
} else if (used < 65) {
ctx = ` \x1b[33m${bar} ${used}%\x1b[0m${tip}`;
} else if (used < 80) {
ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m${tip}`;
} else {
ctx = ` \x1b[5;31m${bar} ${used}%\x1b[0m${tip}`;
}
}
// ── Model label with context size ────────────────────────────
let modelLabel = model;
if (ctxSize) {
const sizeLabel = ctxSize >= 1000000 ? `${Math.round(ctxSize / 1000000)}M` : `${Math.round(ctxSize / 1000)}K`;
modelLabel = `${model} (${sizeLabel} context)`;
}
// ── Asset suggestion from bridge file ────────────────────────
let assetHint = '';
try {
const bridgePath = path.join(os.tmpdir(), 'claude-asset-suggest.json');
if (fs.existsSync(bridgePath)) {
const bridge = JSON.parse(fs.readFileSync(bridgePath, 'utf8'));
if (Date.now() - bridge.ts < 30000 && bridge.top && bridge.top.length > 0) {
const names = bridge.top.slice(0, 3).map(a => a.invoke).join(' ');
assetHint = ` \x1b[2m\u2502\x1b[0m \x1b[36m\ud83d\udca1 ${names}\x1b[0m`;
}
}
} catch (e) {}
// ── Output ───────────────────────────────────────────────────
const dirname = path.basename(dir);
process.stdout.write(
`\x1b[2m${modelLabel}\x1b[0m \u2502 \x1b[2m${dirname}\x1b[0m${ctx}${assetHint}`
);} catch (e) {} });
{ "name": "cc-status-board", "version": "1.1.0", "description": "Smart status bar for Claude Code — context meter, AI asset discovery, and session info at a glance", "main": "install.js", "bin": { "cc-status-board": "install.js" }, "scripts": { "install-global": "node install.js", "build-index": "node src/build-index.js", "test": "node test/test.js" }, "keywords": [ "claude-code", "statusline", "ai-assets", "context-window", "developer-tools", "claude", "mcp", "skills", "hooks" ], "author": "William Wang williamwangai@gmail.com", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/WilliamWangAI/cc-status-board.git" }, "homepage": "https://github.com/WilliamWangAI/cc-status-board", "engines": { "node": ">=18.0.0" } }
CC Status Board
Smart status bar for Claude Code — context meter, AI asset discovery, and session info at a glance.
Install from TokRepo — the original publishing platform for AI assets.
What It Does
CC Status Board adds three features to your Claude Code status bar:
1. Context Meter
See how much of your context window you've used — at a glance. Color-coded: green (< 50%) → yellow (50-65%) → orange (65-80%) → blinking red (80%+).
2. AI Asset Discovery
As you type, the most relevant installed AI assets appear in your status bar. No extra tokens consumed — matching runs 100% locally using intent classification + TF-IDF scoring.
Scans all your installed assets:
- Skills (
~/.claude/skills/) - Agents (
~/.claude/agents/) - Commands (
~/.claude/commands/) - Plugins (Claude Code built-in plugins)
- MCP Servers (
.mcp.json)
Smart matching (not just keyword search):
- 35+ intent categories with CJK + English aliases
- Bilingual keywords — LLM generates Chinese keywords for English assets at index time, so Chinese queries match English tools
- TF-IDF weighting — rare keywords matter more
- Specificity bonus — specialist tools rank above generalists
- Diversity filter — no more than 2 results from the same namespace
3. Model & Directory Info
Always shows your current model, context window size, and working directory.
Opus 4.6 (1M context) │ myproject █░░░░░░░░░ 12%
Sonnet 4.6 (200K context) │ webapp ███░░░░░░░ 28%Install
Recommended: Install from TokRepo for one-click setup.
Or install from source:
git clone https://github.com/henu-wang/cc-status-board.git
cd cc-status-board
node install.jsThen restart Claude Code.
How It Works
┌─────────────────────────────────────────────────┐
│ You type a message │
│ │ │
│ ▼ │
│ UserPromptSubmit hook │
│ → Tokenize (CJK bigrams + Latin words) │
│ → Detect intents (35+ categories) │
│ → Score assets (TF-IDF + specificity) │
│ → Write top 3 to temp bridge file │
│ │ │
│ ▼ │
│ Statusline reads bridge file │
│ → Display: model │ dir │ context% │ 💡 assets │
└─────────────────────────────────────────────────┘Zero token consumption. Everything runs as local Node.js processes.
Rebuild Asset Index
When you install new skills, MCP servers, or plugins:
node ~/.claude/hooks/cc-build-index.jsUninstall
Remove from ~/.claude/settings.json:
- Delete the
statusLineentry - Remove the
cc-asset-suggestentry fromhooks.UserPromptSubmit - Delete the hook files:
rm ~/.claude/hooks/cc-statusline.js
rm ~/.claude/hooks/cc-asset-suggest.js
rm ~/.claude/hooks/cc-build-index.js
rm ~/.claude/cache/asset-index.jsonBilingual Matching (v1.1)
Asset names and descriptions are typically in English. When you type in Chinese, only intent detection would match — keyword and description scoring paths wouldn't fire.
v1.1 fixes this automatically. During index build, the claude CLI generates Chinese keywords for every asset. No API key needed — it uses the same Claude that powers your Claude Code session.
node ~/.claude/hooks/cc-build-index.js # rebuilds with bilingual keywordsThe Chinese keywords are baked into the local index — zero extra cost at query time.
Configuration
The status bar works out of the box. To customize:
| What | How |
|---|---|
| Rebuild bilingual index | node ~/.claude/hooks/cc-build-index.js |
| Change bar width | Edit statusline.js, change Math.floor(used / 10) denominator |
| Add intent categories | Edit build-index.js, add to INTENT_TAXONOMY |
| Change max suggestions | Edit asset-suggest.js, change MAX_SUGGESTIONS |
| Adjust match threshold | Edit asset-suggest.js, change MIN_SCORE |
Requirements
- Claude Code (CLI, desktop, or IDE extension)
- Node.js >= 18
License
MIT
Acknowledgments
Inspired by Get Shit Done (GSD) — the statusline hook architecture and context monitoring patterns originated from this project. If you're building non-trivial projects with Claude Code, GSD is a game changer.
Author
Built by William Wang — founder of TokRepo, GEOScore AI, and KeepRule.