UNPKG

@felixgeelhaar/cclint

Version:

Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.

113 lines (111 loc) 5.08 kB
import { Command } from 'commander'; import { readFileSync } from 'fs'; import { ContextFile } from '../../domain/ContextFile.js'; import { Severity } from '../../domain/Severity.js'; import { RulesEngine } from '../../domain/RulesEngine.js'; import { createRules } from '../../rules/registry/createRules.js'; import { ConfigLoader } from '../../infrastructure/ConfigLoader.js'; import { RULE_METADATA } from '../../infrastructure/RuleMetadata.js'; function severityName(s) { if (s === Severity.ERROR) return 'error'; if (s === Severity.WARNING) return 'warning'; return 'info'; } async function explainViaClaude(apiKey, ruleId, ruleRationale, violation, fileContent, filePath) { const offendingLine = fileContent.split('\n')[violation.line - 1] ?? ''; const prompt = `You are helping a developer fix a CLAUDE.md linter violation. Rule: ${ruleId} Rationale: ${ruleRationale} Violation message: ${violation.message} File: ${filePath} Offending line ${violation.line}: ${offendingLine} Give a concise (3-6 lines) actionable suggestion. Show a concrete rewrite or fix. Do not restate the rule.`; const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 400, messages: [{ role: 'user', content: prompt }], }), }); if (!response.ok) { const body = await response.text(); throw new Error(`Anthropic API error ${response.status}: ${body.slice(0, 200)}`); } const data = (await response.json()); const text = data.content?.find(b => b.type === 'text')?.text; return text ?? '(empty response)'; } export const whyCommand = new Command('why') .description('Explain a violation in plain language and (optionally) get an AI fix suggestion. Pair with --ai and ANTHROPIC_API_KEY for context-aware suggestions.') .argument('<file>', 'Path to the CLAUDE.md file with the violation') .option('-r, --rule <rule-id>', 'Filter to a specific rule id (e.g. "command-safety")') .option('-l, --line <line>', 'Filter to violations on a specific line') .option('--ai', 'Use Anthropic API to generate a context-aware fix. Requires ANTHROPIC_API_KEY env var.') .action(async (file, options) => { try { const content = readFileSync(file, 'utf-8'); const contextFile = new ContextFile(file, content); const engine = new RulesEngine(createRules(ConfigLoader.load())); const result = engine.lint(contextFile); let violations = [...result.violations]; if (options.rule !== undefined) { violations = violations.filter(v => v.ruleId === options.rule); } if (options.line !== undefined) { const lineNum = parseInt(options.line, 10); violations = violations.filter(v => v.location.line === lineNum); } if (violations.length === 0) { console.log('No matching violations found.'); return; } const apiKey = process.env['ANTHROPIC_API_KEY']; const useAi = options.ai === true; if (useAi && (apiKey === undefined || apiKey === '')) { console.error('Error: --ai requires ANTHROPIC_API_KEY environment variable.'); process.exit(1); } for (const v of violations) { const meta = RULE_METADATA[v.ruleId]; console.log(`\n[${severityName(v.severity).toUpperCase()}] ${v.ruleId} at ${v.location.line}:${v.location.column}`); console.log(` ${v.message}`); if (meta) { console.log(`\nWhy this rule exists:`); console.log(` ${meta.rationale}`); if (meta.goodExamples[0]) { console.log(`\nGood example:`); console.log(` ${meta.goodExamples[0].code}`); console.log(` → ${meta.goodExamples[0].explanation}`); } } if (useAi && apiKey !== undefined) { try { const suggestion = await explainViaClaude(apiKey, v.ruleId, meta?.rationale ?? '', { message: v.message, line: v.location.line }, content, file); console.log(`\nAI suggestion:`); console.log(suggestion .split('\n') .map(l => ` ${l}`) .join('\n')); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`\n AI suggestion unavailable: ${msg}`); } } } } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`Error: ${msg}`); process.exit(1); } }); //# sourceMappingURL=why.js.map