Esta página se muestra en inglés. Una traducción al español está en curso.
CLI ToolsApr 7, 2026·1 min de lectura

CC Status Board

Smart status bar for Claude Code — context meter, AI asset discovery with bilingual matching, and session info at a glance. v1.1: Uses claude CLI to auto-generate Chinese keywords at index time. Zero config, zero extra API keys.

#!/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.

Context Meter

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.

Asset Discovery

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.js

Then 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.js

Uninstall

Remove from ~/.claude/settings.json:

  1. Delete the statusLine entry
  2. Remove the cc-asset-suggest entry from hooks.UserPromptSubmit
  3. 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.json

Bilingual 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 keywords

The 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.

Discusión

Inicia sesión para unirte a la discusión.
Aún no hay comentarios. Sé el primero en compartir tus ideas.