Apr 7, 2026·3 min read

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.

HE
henuwangkai · Community
Quick Use

Use it first, then decide how deep to go

This block should tell both the user and the agent what to copy, install, and apply first.

install.js

View source (106 lines)
#!/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}
`);

src/build-index.js

View source (453 lines)
#!/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();

src/asset-suggest.js

View source (157 lines)
#!/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);
  }
});

src/statusline.js

View source (84 lines)
#!/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) {}
});

package.json

View source (35 lines)
{
  "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.

Discussion

Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.