UNPKG

@felixgeelhaar/cclint

Version:

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

83 lines 3.75 kB
import { ContextFile } from '../domain/ContextFile.js'; import { Violation } from '../domain/Violation.js'; import { Location } from '../domain/Location.js'; import { Severity } from '../domain/Severity.js'; import { FrontmatterParser, Frontmatter } from './support/FrontmatterParser.js'; export class SkillStructureRule { id = 'skill-structure'; description = 'Validates Claude Code skill structure and frontmatter'; requireDescription; maxDescriptionLength; minDescriptionLength = 10; constructor(options) { this.requireDescription = options?.requireDescription ?? true; this.maxDescriptionLength = options?.maxDescriptionLength ?? 200; } lint(file) { const violations = []; if (!this.isSkillFile(file.path)) { return violations; } const frontmatter = FrontmatterParser.parse(file.lines); violations.push(...this.validateFrontmatter(frontmatter)); violations.push(...this.validateStructure(file.lines)); return violations; } isSkillFile(path) { return path.includes('.claude/skills/') && path.endsWith('.md'); } validateFrontmatter(frontmatter) { const violations = []; const name = frontmatter.getString('name'); const description = frontmatter.getString('description'); if (!frontmatter.hasFence) { violations.push(new Violation(this.id, 'Skill file is missing frontmatter. Add --- at the start with name and description.', Severity.ERROR, new Location(1, 1))); return violations; } if (!name) { violations.push(new Violation(this.id, 'Skill frontmatter is missing required "name" field.', Severity.ERROR, new Location(1, 1))); } else if (!this.isValidSkillName(name)) { violations.push(new Violation(this.id, `Skill name "${name}" should use kebab-case (lowercase with hyphens).`, Severity.ERROR, new Location(1, 1))); } if (!description) { if (this.requireDescription) { violations.push(new Violation(this.id, 'Skill frontmatter is missing required "description" field.', Severity.ERROR, new Location(1, 1))); } } else { const descLength = description.length; if (descLength < this.minDescriptionLength) { violations.push(new Violation(this.id, `Skill description is too short (${descLength} chars). Minimum is ${this.minDescriptionLength} characters.`, Severity.WARNING, new Location(1, 1))); } if (descLength > this.maxDescriptionLength) { violations.push(new Violation(this.id, `Skill description is too long (${descLength} chars). Maximum is ${this.maxDescriptionLength} characters.`, Severity.WARNING, new Location(1, 1))); } } return violations; } isValidSkillName(name) { return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name); } validateStructure(lines) { const violations = []; let lineNumber = 0; let foundContent = false; for (const line of lines) { lineNumber++; const trimmed = line.trim(); if (trimmed === '---') { continue; } if (trimmed.length > 0 && !trimmed.startsWith('#')) { foundContent = true; break; } } if (!foundContent) { violations.push(new Violation(this.id, 'Skill file appears empty after frontmatter. Add content describing the skill.', Severity.WARNING, new Location(lineNumber, 1))); } return violations; } } //# sourceMappingURL=SkillStructureRule.js.map