@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.
- package/README.md +84 -2
- package/dist/{chunk-WEYNMCAH.js → chunk-FHTSMC5D.js} +1033 -45
- package/dist/{cli-VYWAONGX.js → cli-7IBRPDA6.js} +118 -24
- package/dist/index.js +2 -2
- package/dist/mcp/server.js +1083 -52
- package/dist/{server-7C2IQ7VV.js → server-IFZ3VEK3.js} +47 -2
- package/package.json +16 -10
|
@@ -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-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
5
|
+
await import("./server-IFZ3VEK3.js");
|
|
6
6
|
} else {
|
|
7
|
-
const { runCli } = await import("./cli-
|
|
7
|
+
const { runCli } = await import("./cli-7IBRPDA6.js");
|
|
8
8
|
await runCli();
|
|
9
9
|
}
|