@yawlabs/ctxlint 0.9.10 → 0.9.12
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/.pre-commit-hooks.yaml +4 -1
- package/AGENT_SESSION_LINT_SPEC.md +1 -1
- package/MCP_CONFIG_LINT_SPEC.md +1 -1
- package/README.md +3 -3
- package/dist/index.js +60 -27
- package/package.json +2 -2
package/.pre-commit-hooks.yaml
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
- id: ctxlint
|
|
2
2
|
name: ctxlint
|
|
3
3
|
description: Lint AI agent context files against your actual codebase
|
|
4
|
-
|
|
4
|
+
# Version-pinned so a checkout at `rev: vX.Y.Z` runs exactly that release
|
|
5
|
+
# of ctxlint — matches the pinning done by `ctxlint init`. release.sh keeps
|
|
6
|
+
# this in sync with package.json on each bump.
|
|
7
|
+
entry: npx @yawlabs/ctxlint@0.9.12 --strict
|
|
5
8
|
language: node
|
|
6
9
|
always_run: true
|
|
7
10
|
pass_filenames: false
|
|
@@ -15,7 +15,7 @@ This specification defines a standard set of lint rules for validating agent ses
|
|
|
15
15
|
|
|
16
16
|
The specification includes:
|
|
17
17
|
- A reference of session data locations across 8 AI coding agents
|
|
18
|
-
-
|
|
18
|
+
- 7 lint rules in the `session` category with defined severities
|
|
19
19
|
- A machine-readable rule catalog ([`agent-session-lint-rules.json`](./agent-session-lint-rules.json))
|
|
20
20
|
- Sibling-repo detection for cross-project checks
|
|
21
21
|
|
package/MCP_CONFIG_LINT_SPEC.md
CHANGED
|
@@ -16,7 +16,7 @@ This specification defines a standard set of lint rules for validating MCP serve
|
|
|
16
16
|
|
|
17
17
|
The specification includes:
|
|
18
18
|
- A complete reference of MCP config file locations, formats, and client-specific behaviors
|
|
19
|
-
-
|
|
19
|
+
- 43 lint rules organized into 8 categories with defined severities
|
|
20
20
|
- A machine-readable rule catalog ([`mcp-config-lint-rules.json`](./mcp-config-lint-rules.json))
|
|
21
21
|
- Auto-fix definitions for rules that support automated correction
|
|
22
22
|
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Multiply that across a team with 5 context files, 3 MCP configs, and 2 people wh
|
|
|
20
20
|
|
|
21
21
|
ctxlint is a linter purpose-built for this. It reads your context files, cross-references them against your actual codebase, and catches the drift before your agent does.
|
|
22
22
|
|
|
23
|
-
- **Instant startup** — ships as a single self-contained bundle with zero runtime dependencies. `npx` downloads ~
|
|
23
|
+
- **Instant startup** — ships as a single self-contained bundle with zero runtime dependencies. `npx` downloads a ~400 KB tarball and starts immediately
|
|
24
24
|
- **Catches real problems** — broken paths, wrong commands, stale references, contradictions across files
|
|
25
25
|
- **Smart suggestions** — detects git renames and fuzzy-matches to suggest the right path
|
|
26
26
|
- **Auto-fix** — `--fix` rewrites broken paths automatically using git history
|
|
@@ -215,7 +215,7 @@ Session checks are **opt-in** because they access files outside the project dire
|
|
|
215
215
|
## Example Output
|
|
216
216
|
|
|
217
217
|
```
|
|
218
|
-
ctxlint v0.9.
|
|
218
|
+
ctxlint v0.9.10
|
|
219
219
|
|
|
220
220
|
Scanning /Users/you/my-app...
|
|
221
221
|
|
|
@@ -351,7 +351,7 @@ Add to your `.pre-commit-config.yaml`:
|
|
|
351
351
|
```yaml
|
|
352
352
|
repos:
|
|
353
353
|
- repo: https://github.com/yawlabs/ctxlint
|
|
354
|
-
rev: v0.9.
|
|
354
|
+
rev: v0.9.10
|
|
355
355
|
hooks:
|
|
356
356
|
- id: ctxlint
|
|
357
357
|
```
|
package/dist/index.js
CHANGED
|
@@ -34559,7 +34559,7 @@ function parseSections(lines) {
|
|
|
34559
34559
|
if (sections.length > 0) {
|
|
34560
34560
|
const prev = sections[sections.length - 1];
|
|
34561
34561
|
if (prev.endLine === -1) {
|
|
34562
|
-
prev.endLine = i2
|
|
34562
|
+
prev.endLine = i2;
|
|
34563
34563
|
}
|
|
34564
34564
|
}
|
|
34565
34565
|
sections.push({
|
|
@@ -41404,10 +41404,16 @@ function loadSettingsSources(projectRoot) {
|
|
|
41404
41404
|
path6.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
|
|
41405
41405
|
];
|
|
41406
41406
|
for (const p2 of candidates) {
|
|
41407
|
+
let content;
|
|
41407
41408
|
try {
|
|
41408
|
-
|
|
41409
|
-
sources.push(JSON.parse(content));
|
|
41409
|
+
content = fs5.readFileSync(p2, "utf-8");
|
|
41410
41410
|
} catch {
|
|
41411
|
+
continue;
|
|
41412
|
+
}
|
|
41413
|
+
try {
|
|
41414
|
+
sources.push(JSON.parse(content));
|
|
41415
|
+
} catch (err) {
|
|
41416
|
+
console.warn(`ctxlint: could not parse ${p2}: ${err.message}`);
|
|
41411
41417
|
}
|
|
41412
41418
|
}
|
|
41413
41419
|
return sources;
|
|
@@ -41609,10 +41615,11 @@ async function checkRedundancy(file2, projectRoot) {
|
|
|
41609
41615
|
}
|
|
41610
41616
|
function checkDuplicateContent(files) {
|
|
41611
41617
|
const issues = [];
|
|
41618
|
+
const DUPLICATE_CONTENT_THRESHOLD = 0.6;
|
|
41612
41619
|
for (let i2 = 0; i2 < files.length; i2++) {
|
|
41613
41620
|
for (let j3 = i2 + 1; j3 < files.length; j3++) {
|
|
41614
41621
|
const overlap = calculateLineOverlap(files[i2].content, files[j3].content);
|
|
41615
|
-
if (overlap
|
|
41622
|
+
if (overlap >= DUPLICATE_CONTENT_THRESHOLD) {
|
|
41616
41623
|
issues.push({
|
|
41617
41624
|
severity: "warning",
|
|
41618
41625
|
check: "redundancy",
|
|
@@ -41634,11 +41641,12 @@ function calculateLineOverlap(contentA, contentB) {
|
|
|
41634
41641
|
contentB.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
|
|
41635
41642
|
);
|
|
41636
41643
|
if (linesA.size === 0 || linesB.size === 0) return 0;
|
|
41637
|
-
let
|
|
41644
|
+
let intersection2 = 0;
|
|
41638
41645
|
for (const line of linesA) {
|
|
41639
|
-
if (linesB.has(line))
|
|
41646
|
+
if (linesB.has(line)) intersection2++;
|
|
41640
41647
|
}
|
|
41641
|
-
|
|
41648
|
+
const unionSize = linesA.size + linesB.size - intersection2;
|
|
41649
|
+
return intersection2 / unionSize;
|
|
41642
41650
|
}
|
|
41643
41651
|
function escapeRegex2(str) {
|
|
41644
41652
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -43845,7 +43853,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
43845
43853
|
import { resolve as resolve8, dirname as dirname3 } from "node:path";
|
|
43846
43854
|
import { fileURLToPath } from "node:url";
|
|
43847
43855
|
function loadVersion() {
|
|
43848
|
-
if (true) return "0.9.
|
|
43856
|
+
if (true) return "0.9.12";
|
|
43849
43857
|
const __dir = dirname3(fileURLToPath(import.meta.url));
|
|
43850
43858
|
const pkgPath = resolve8(__dir, "../package.json");
|
|
43851
43859
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
@@ -44721,7 +44729,7 @@ function applyFixes(result, options = {}) {
|
|
|
44721
44729
|
let line = lines[lineIdx];
|
|
44722
44730
|
for (const fix of lineFixes) {
|
|
44723
44731
|
if (line.includes(fix.oldText)) {
|
|
44724
|
-
line = line.
|
|
44732
|
+
line = line.replaceAll(fix.oldText, fix.newText);
|
|
44725
44733
|
totalFixes++;
|
|
44726
44734
|
const prefix = dryRun ? source_default.cyan(" Would fix") : source_default.green(" Fixed");
|
|
44727
44735
|
log(
|
|
@@ -44917,8 +44925,11 @@ var init_server3 = __esm({
|
|
|
44917
44925
|
checks: external_exports3.array(checkEnum).optional().describe("Which checks to run before fixing. Defaults to all.")
|
|
44918
44926
|
},
|
|
44919
44927
|
{
|
|
44928
|
+
// ctxlint_fix writes to disk via applyFixes() → fs.writeFileSync, so it
|
|
44929
|
+
// must advertise destructiveHint: true. Hosts (Claude Code, Cursor, etc.)
|
|
44930
|
+
// use this to decide whether to require user confirmation before the call.
|
|
44920
44931
|
readOnlyHint: false,
|
|
44921
|
-
destructiveHint:
|
|
44932
|
+
destructiveHint: true,
|
|
44922
44933
|
idempotentHint: true,
|
|
44923
44934
|
openWorldHint: false
|
|
44924
44935
|
},
|
|
@@ -51627,6 +51638,9 @@ function formatTokenReport(result) {
|
|
|
51627
51638
|
lines.push("");
|
|
51628
51639
|
return lines.join("\n");
|
|
51629
51640
|
}
|
|
51641
|
+
function isSyntheticPath(p2) {
|
|
51642
|
+
return p2.startsWith("(") || p2.startsWith("~");
|
|
51643
|
+
}
|
|
51630
51644
|
function formatSarif(result) {
|
|
51631
51645
|
const severityToLevel = {
|
|
51632
51646
|
error: "error",
|
|
@@ -51636,25 +51650,31 @@ function formatSarif(result) {
|
|
|
51636
51650
|
const results = [];
|
|
51637
51651
|
for (const file2 of result.files) {
|
|
51638
51652
|
for (const issue2 of file2.issues) {
|
|
51653
|
+
const location = isSyntheticPath(file2.path) ? {
|
|
51654
|
+
logicalLocations: [
|
|
51655
|
+
{
|
|
51656
|
+
name: file2.path,
|
|
51657
|
+
kind: "resource"
|
|
51658
|
+
}
|
|
51659
|
+
]
|
|
51660
|
+
} : {
|
|
51661
|
+
physicalLocation: {
|
|
51662
|
+
artifactLocation: {
|
|
51663
|
+
uri: file2.path,
|
|
51664
|
+
uriBaseId: "%SRCROOT%"
|
|
51665
|
+
},
|
|
51666
|
+
region: {
|
|
51667
|
+
startLine: Math.max(issue2.line, 1)
|
|
51668
|
+
}
|
|
51669
|
+
}
|
|
51670
|
+
};
|
|
51639
51671
|
const sarifResult = {
|
|
51640
51672
|
ruleId: `ctxlint/${issue2.check}`,
|
|
51641
51673
|
level: severityToLevel[issue2.severity] || "note",
|
|
51642
51674
|
message: {
|
|
51643
51675
|
text: issue2.message + (issue2.suggestion ? ` (${issue2.suggestion})` : "")
|
|
51644
51676
|
},
|
|
51645
|
-
locations: [
|
|
51646
|
-
{
|
|
51647
|
-
physicalLocation: {
|
|
51648
|
-
artifactLocation: {
|
|
51649
|
-
uri: file2.path,
|
|
51650
|
-
uriBaseId: "%SRCROOT%"
|
|
51651
|
-
},
|
|
51652
|
-
region: {
|
|
51653
|
-
startLine: Math.max(issue2.line, 1)
|
|
51654
|
-
}
|
|
51655
|
-
}
|
|
51656
|
-
}
|
|
51657
|
-
]
|
|
51677
|
+
locations: [location]
|
|
51658
51678
|
};
|
|
51659
51679
|
if (issue2.detail) {
|
|
51660
51680
|
sarifResult.message.text += `
|
|
@@ -52174,6 +52194,7 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
52174
52194
|
path11.join(resolvedPath, ".continue"),
|
|
52175
52195
|
path11.join(resolvedPath, ".vscode")
|
|
52176
52196
|
];
|
|
52197
|
+
const watchers = [];
|
|
52177
52198
|
let debounceTimer = null;
|
|
52178
52199
|
const rerun = () => {
|
|
52179
52200
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -52215,16 +52236,28 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
52215
52236
|
};
|
|
52216
52237
|
for (const filePath of watchPaths) {
|
|
52217
52238
|
try {
|
|
52218
|
-
fs9.watch(filePath, rerun);
|
|
52239
|
+
watchers.push(fs9.watch(filePath, rerun));
|
|
52219
52240
|
} catch {
|
|
52220
52241
|
}
|
|
52221
52242
|
}
|
|
52222
52243
|
for (const dir of watchDirs) {
|
|
52223
52244
|
try {
|
|
52224
|
-
fs9.watch(dir, { recursive: true }, rerun);
|
|
52245
|
+
watchers.push(fs9.watch(dir, { recursive: true }, rerun));
|
|
52225
52246
|
} catch {
|
|
52226
52247
|
}
|
|
52227
52248
|
}
|
|
52249
|
+
const shutdown = () => {
|
|
52250
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
52251
|
+
for (const w of watchers) {
|
|
52252
|
+
try {
|
|
52253
|
+
w.close();
|
|
52254
|
+
} catch {
|
|
52255
|
+
}
|
|
52256
|
+
}
|
|
52257
|
+
process.exit(0);
|
|
52258
|
+
};
|
|
52259
|
+
process.once("SIGINT", shutdown);
|
|
52260
|
+
process.once("SIGTERM", shutdown);
|
|
52228
52261
|
await new Promise(() => {
|
|
52229
52262
|
});
|
|
52230
52263
|
}
|
|
@@ -52243,11 +52276,11 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
52243
52276
|
const hookPath = path11.join(hooksDir, "pre-commit");
|
|
52244
52277
|
const fullHookContent = `#!/bin/sh
|
|
52245
52278
|
# ctxlint pre-commit hook
|
|
52246
|
-
npx @yawlabs/ctxlint --strict
|
|
52279
|
+
npx @yawlabs/ctxlint@${VERSION} --strict
|
|
52247
52280
|
`;
|
|
52248
52281
|
const appendHookContent = `
|
|
52249
52282
|
# ctxlint pre-commit hook
|
|
52250
|
-
npx @yawlabs/ctxlint --strict
|
|
52283
|
+
npx @yawlabs/ctxlint@${VERSION} --strict
|
|
52251
52284
|
`;
|
|
52252
52285
|
if (fs9.existsSync(hookPath)) {
|
|
52253
52286
|
const existing = fs9.readFileSync(hookPath, "utf-8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/ctxlint",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"description": "Lint your AI agent context files, MCP server configs, and session data against your actual codebase",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ctxlint": "dist/index.js"
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"esbuild"
|
|
104
104
|
],
|
|
105
105
|
"overrides": {
|
|
106
|
-
"hono": "^4.12.
|
|
106
|
+
"hono": "^4.12.14",
|
|
107
107
|
"@hono/node-server": "^1.19.13",
|
|
108
108
|
"vite": "^8.0.8"
|
|
109
109
|
}
|