2026年4月7日·1 分钟阅读

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
快速使用

先拿来用,再决定要不要深挖

这里应该同时让用户和 Agent 知道第一步该复制什么、安装什么、落到哪里。

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.

讨论

登录后参与讨论。
还没有评论,来写第一条吧。