UNPKG

@felixgeelhaar/cclint

Version:

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

123 lines 4.6 kB
import { assertWithinContentLimits } from './ContentLimits.js'; export class ContextFile { path; content; lines; constructor(path, content) { if (path.trim() === '') { throw new Error('File path cannot be empty'); } // Split on all line-ending conventions (CRLF, lone CR, LF) so a `line` // never retains a trailing carriage return, which would break // line-anchored rule logic (regex `$`/`.`, `endsWith`, `trimEnd`). The raw // `content` is preserved intact — only the `lines` view is normalized. const lines = content.split(/\r\n|\r|\n/); // Enforce the DoS caps here so every entrypoint (CLI, GitHub Action, MCP) // is protected, regardless of how the content was obtained. assertWithinContentLimits(content, path, lines); this.path = path; this.content = content; this.lines = lines; } getLineCount() { return this.lines.length; } getCharacterCount() { return this.content.length; } getLine(lineNumber) { if (lineNumber <= 0) { throw new Error('Line number must be positive'); } if (lineNumber > this.lines.length) { throw new Error(`Line number ${lineNumber} is out of range`); } return this.lines[lineNumber - 1] ?? ''; } /** * Whether this file is a Markdown document (`.md` / `.markdown`). * * @remarks * Used by rules that validate Markdown-document structure so they only run * on Markdown files, not on other linted config (e.g. `settings.json`). */ isMarkdown() { return /\.(md|markdown)$/i.test(this.path); } /** * Whether this file is a Claude Code settings file * (`settings.json` / `settings.local.json`). */ isSettingsFile() { return /(^|[\\/])settings(\.local)?\.json$/i.test(this.path); } /** * Whether this file is a Claude Code plugin manifest — either a plugin * descriptor (`plugin.json`, conventionally under `.claude-plugin/`) or a * marketplace listing (`marketplace.json`). * * @remarks * Matched by basename so both the plugin and marketplace manifests are * covered wherever they live, while ordinary `package.json` / `tsconfig.json` * files are left untouched. */ isPluginManifest() { return /(^|[\\/])(plugin|marketplace)\.json$/i.test(this.path); } /** * Whether this file is a Claude Code MCP server configuration (`.mcp.json`). * * @remarks * Matches the dotfile `.mcp.json` and any `*.mcp.json`, but not a plain * `mcp.json` without the leading dot or a generic `*.json`. */ isMcpConfig() { return /\.mcp\.json$/i.test(this.path); } /** * Whether this file is a Claude Code output style * (a Markdown file under an `output-styles/` directory). */ isOutputStyle() { return /(^|[\\/])output-styles[\\/].+\.(md|markdown)$/i.test(this.path); } /** * Whether this file is a Claude Code skill (a Markdown file under a * `.claude/skills/` directory). */ isSkillFile() { return /(^|[\\/])\.claude[\\/]skills[\\/].+\.(md|markdown)$/i.test(this.path); } /** * Whether this file is a Claude Code subagent (a Markdown file under a * `.claude/agents/` directory). */ isAgentFile() { return /(^|[\\/])\.claude[\\/]agents[\\/].+\.(md|markdown)$/i.test(this.path); } /** * Whether this file is a CLAUDE.md-style context document — a Markdown file * that is NOT a skill, subagent, or output-style. * * @remarks * Used by rules that validate CLAUDE.md *document* structure (required * sections, monorepo hierarchy, file location, opinionated guidance). Those * rules must not fire on skill / subagent / output-style Markdown, which are * Markdown but not CLAUDE.md documents — otherwise a project-wide lint spams * "missing section" false positives on every skill and agent file. */ isClaudeMarkdown() { return (this.isMarkdown() && !this.isSkillFile() && !this.isAgentFile() && !this.isOutputStyle()); } hasSection(sectionTitle) { const headerRegex = new RegExp(`^#{1,6}\\s+${this.escapeRegExp(sectionTitle)}\\s*$`, 'm'); return headerRegex.test(this.content); } escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } } //# sourceMappingURL=ContextFile.js.map