@yawlabs/ctxlint 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ALL_CHECKS,
4
+ ALL_MCP_CHECKS,
4
5
  VERSION,
5
6
  applyFixes,
6
7
  freeEncoder,
@@ -9,7 +10,7 @@ import {
9
10
  resetTokenThresholds,
10
11
  runAudit,
11
12
  setTokenThresholds
12
- } from "./chunk-WEYNMCAH.js";
13
+ } from "./chunk-FHTSMC5D.js";
13
14
 
14
15
  // src/cli.ts
15
16
  import * as fs2 from "fs";
@@ -25,30 +26,61 @@ function formatText(result, verbose = false) {
25
26
  lines.push("");
26
27
  lines.push(`Scanning ${result.projectRoot}...`);
27
28
  lines.push("");
29
+ const isMcpFile = (f) => f.issues.some((i) => i.check.startsWith("mcp-"));
30
+ const contextFiles = result.files.filter((f) => !isMcpFile(f));
31
+ const mcpFiles = result.files.filter((f) => isMcpFile(f));
28
32
  const totalTokens = result.summary.totalTokens;
29
- lines.push(
30
- `Found ${result.files.length} context file${result.files.length !== 1 ? "s" : ""} (${totalTokens.toLocaleString()} tokens total)`
31
- );
32
- for (const file of result.files) {
33
- let desc = ` ${file.path} (${file.tokens.toLocaleString()} tokens, ${file.lines} lines)`;
34
- if (file.isSymlink && file.symlinkTarget) {
35
- desc = ` ${file.path} ${chalk.dim(`\u2192 ${file.symlinkTarget} (symlink)`)}`;
33
+ if (contextFiles.length > 0) {
34
+ lines.push(
35
+ `Found ${contextFiles.length} context file${contextFiles.length !== 1 ? "s" : ""} (${totalTokens.toLocaleString()} tokens total)`
36
+ );
37
+ for (const file of contextFiles) {
38
+ let desc = ` ${file.path} (${file.tokens.toLocaleString()} tokens, ${file.lines} lines)`;
39
+ if (file.isSymlink && file.symlinkTarget) {
40
+ desc = ` ${file.path} ${chalk.dim(`\u2192 ${file.symlinkTarget} (symlink)`)}`;
41
+ }
42
+ lines.push(desc);
43
+ }
44
+ }
45
+ if (mcpFiles.length > 0) {
46
+ if (contextFiles.length > 0) lines.push("");
47
+ lines.push(`Found ${mcpFiles.length} MCP config${mcpFiles.length !== 1 ? "s" : ""}`);
48
+ for (const file of mcpFiles) {
49
+ lines.push(` ${file.path}`);
36
50
  }
37
- lines.push(desc);
51
+ }
52
+ if (contextFiles.length === 0 && mcpFiles.length === 0) {
53
+ lines.push(`Found ${result.files.length} file${result.files.length !== 1 ? "s" : ""}`);
38
54
  }
39
55
  lines.push("");
40
- for (const file of result.files) {
41
- const fileIssues = file.issues;
42
- if (fileIssues.length === 0 && !verbose) continue;
43
- lines.push(chalk.underline(file.path));
44
- if (fileIssues.length === 0) {
45
- lines.push(chalk.green(" \u2713 All checks passed"));
46
- } else {
47
- for (const issue of fileIssues) {
48
- lines.push(formatIssue(issue));
56
+ const renderFileGroup = (files) => {
57
+ for (const file of files) {
58
+ const fileIssues = file.issues;
59
+ if (fileIssues.length === 0 && !verbose) continue;
60
+ lines.push(chalk.underline(file.path));
61
+ if (fileIssues.length === 0) {
62
+ lines.push(chalk.green(" \u2713 All checks passed"));
63
+ } else {
64
+ for (const issue of fileIssues) {
65
+ lines.push(formatIssue(issue));
66
+ }
49
67
  }
68
+ lines.push("");
50
69
  }
51
- lines.push("");
70
+ };
71
+ if (contextFiles.length > 0 && mcpFiles.length > 0) {
72
+ const contextWithIssues = contextFiles.filter((f) => f.issues.length > 0 || verbose);
73
+ const mcpWithIssues = mcpFiles.filter((f) => f.issues.length > 0 || verbose);
74
+ if (contextWithIssues.length > 0) {
75
+ lines.push(chalk.bold("Context Files"));
76
+ renderFileGroup(contextFiles);
77
+ }
78
+ if (mcpWithIssues.length > 0) {
79
+ lines.push(chalk.bold("MCP Configs"));
80
+ renderFileGroup(mcpFiles);
81
+ }
82
+ } else {
83
+ renderFileGroup(result.files);
52
84
  }
53
85
  const { errors, warnings, info } = result.summary;
54
86
  const parts = [];
@@ -113,7 +145,7 @@ function formatSarif(result) {
113
145
  for (const file of result.files) {
114
146
  for (const issue of file.issues) {
115
147
  const sarifResult = {
116
- ruleId: `ctxlint/${issue.check}`,
148
+ ruleId: issue.ruleId ? `ctxlint/${issue.check}/${issue.ruleId}` : `ctxlint/${issue.check}`,
117
149
  level: severityToLevel[issue.severity] || "note",
118
150
  message: {
119
151
  text: issue.message + (issue.suggestion ? ` (${issue.suggestion})` : "")
@@ -194,6 +226,46 @@ function buildRuleDescriptors() {
194
226
  id: "ctxlint/frontmatter",
195
227
  shortDescription: { text: "Invalid or missing frontmatter" },
196
228
  helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
229
+ },
230
+ {
231
+ id: "ctxlint/mcp-schema",
232
+ shortDescription: { text: "MCP config structural validation error" },
233
+ helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
234
+ },
235
+ {
236
+ id: "ctxlint/mcp-security",
237
+ shortDescription: { text: "Hardcoded secret in MCP config" },
238
+ helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
239
+ },
240
+ {
241
+ id: "ctxlint/mcp-commands",
242
+ shortDescription: { text: "MCP stdio command validation issue" },
243
+ helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
244
+ },
245
+ {
246
+ id: "ctxlint/mcp-deprecated",
247
+ shortDescription: { text: "Deprecated MCP transport or pattern" },
248
+ helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
249
+ },
250
+ {
251
+ id: "ctxlint/mcp-env",
252
+ shortDescription: { text: "MCP environment variable syntax issue" },
253
+ helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
254
+ },
255
+ {
256
+ id: "ctxlint/mcp-urls",
257
+ shortDescription: { text: "MCP URL validation issue" },
258
+ helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
259
+ },
260
+ {
261
+ id: "ctxlint/mcp-consistency",
262
+ shortDescription: { text: "MCP config inconsistency across clients" },
263
+ helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
264
+ },
265
+ {
266
+ id: "ctxlint/mcp-redundancy",
267
+ shortDescription: { text: "Redundant MCP config entry" },
268
+ helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
197
269
  }
198
270
  ];
199
271
  }
@@ -235,13 +307,29 @@ async function runCli() {
235
307
  const program = new Command();
236
308
  program.name("ctxlint").description(
237
309
  "Lint your AI agent context files (CLAUDE.md, AGENTS.md, etc.) against your actual codebase"
238
- ).version(VERSION).argument("[path]", "Project directory to scan", ".").option("--strict", "Exit code 1 on any warning or error (for CI)", false).option("--checks <checks>", "Comma-separated list of checks to run", "").option("--format <format>", "Output format: text, json, or sarif", "text").option("--tokens", "Show token breakdown per file", false).option("--verbose", "Show passing checks too", false).option("--fix", "Auto-fix broken paths using git history and fuzzy matching", false).option("--ignore <checks>", "Comma-separated list of checks to ignore", "").option("--quiet", "Suppress all output except errors (exit code only)", false).option("--config <path>", "Path to config file (default: .ctxlintrc in project root)").option("--depth <n>", "Max subdirectory depth to scan (default: 2)", "2").action(async (projectPath, opts) => {
310
+ ).version(VERSION).argument("[path]", "Project directory to scan", ".").option("--strict", "Exit code 1 on any warning or error (for CI)", false).option("--checks <checks>", "Comma-separated list of checks to run", "").option("--format <format>", "Output format: text, json, or sarif", "text").option("--tokens", "Show token breakdown per file", false).option("--verbose", "Show passing checks too", false).option("--fix", "Auto-fix broken paths using git history and fuzzy matching", false).option("--ignore <checks>", "Comma-separated list of checks to ignore", "").option("--quiet", "Suppress all output except errors (exit code only)", false).option("--config <path>", "Path to config file (default: .ctxlintrc in project root)").option("--depth <n>", "Max subdirectory depth to scan (default: 2)", "2").option("--mcp", "Enable MCP config linting alongside context file checks", false).option("--mcp-only", "Run only MCP config checks, skip context file checks", false).option("--mcp-global", "Also scan user/global MCP config files (implies --mcp)", false).action(async (projectPath, opts) => {
239
311
  const resolvedPath = path2.resolve(projectPath);
240
312
  const configPath = opts.config ? path2.resolve(opts.config) : void 0;
241
313
  const config = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
314
+ const mcpGlobal = opts.mcpGlobal || false;
315
+ const mcpOnly = opts.mcpOnly || false;
316
+ const mcpFlag = opts.mcp || mcpGlobal || mcpOnly || config?.mcp || false;
317
+ const explicitChecks = opts.checks ? opts.checks.split(",").map((c) => c.trim()) : null;
318
+ const hasMcpInChecks = explicitChecks?.some((c) => c.startsWith("mcp-")) || false;
319
+ const effectiveMcp = mcpFlag || hasMcpInChecks;
320
+ let checks;
321
+ if (explicitChecks) {
322
+ checks = explicitChecks;
323
+ } else if (mcpOnly) {
324
+ checks = ALL_MCP_CHECKS;
325
+ } else if (effectiveMcp) {
326
+ checks = [...config?.checks || ALL_CHECKS, ...ALL_MCP_CHECKS];
327
+ } else {
328
+ checks = config?.checks || ALL_CHECKS;
329
+ }
242
330
  const options = {
243
331
  projectPath: resolvedPath,
244
- checks: opts.checks ? opts.checks.split(",").map((c) => c.trim()) : config?.checks || ALL_CHECKS,
332
+ checks,
245
333
  strict: opts.strict || config?.strict || false,
246
334
  format: opts.format,
247
335
  verbose: opts.verbose,
@@ -249,7 +337,10 @@ async function runCli() {
249
337
  ignore: opts.ignore ? opts.ignore.split(",").map((c) => c.trim()) : config?.ignore || [],
250
338
  tokensOnly: opts.tokens,
251
339
  quiet: opts.quiet,
252
- depth: parseInt(opts.depth, 10) || 2
340
+ depth: parseInt(opts.depth, 10) || 2,
341
+ mcp: effectiveMcp,
342
+ mcpOnly,
343
+ mcpGlobal: mcpGlobal || config?.mcpGlobal || false
253
344
  };
254
345
  if (config?.tokenThresholds) {
255
346
  setTokenThresholds(config.tokenThresholds);
@@ -260,7 +351,10 @@ async function runCli() {
260
351
  if (spinner) spinner.text = "Running checks...";
261
352
  const result = await runAudit(resolvedPath, activeChecks, {
262
353
  depth: options.depth,
263
- extraPatterns: config?.contextFiles
354
+ extraPatterns: config?.contextFiles,
355
+ mcp: options.mcp,
356
+ mcpGlobal: options.mcpGlobal,
357
+ mcpOnly: options.mcpOnly
264
358
  });
265
359
  spinner?.stop();
266
360
  if (result.files.length === 0) {
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  if (process.argv.includes("--mcp")) {
5
- await import("./server-7C2IQ7VV.js");
5
+ await import("./server-IFZ3VEK3.js");
6
6
  } else {
7
- const { runCli } = await import("./cli-VYWAONGX.js");
7
+ const { runCli } = await import("./cli-7IBRPDA6.js");
8
8
  await runCli();
9
9
  }