@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
JavaScript
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