@yawlabs/ctxlint 0.9.9 → 0.9.11
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 +2 -2
- package/README.md +3 -3
- package/dist/index.js +58 -24
- 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.11 --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
|
@@ -31,7 +31,7 @@ This spec is part of a family of open specifications maintained by Yaw Labs for
|
|
|
31
31
|
| Spec | Scope | Input |
|
|
32
32
|
|---|---|---|
|
|
33
33
|
| **mcp-config-lint** (this spec) | Static analysis of MCP client config files | `.cursor/mcp.json`, `.vscode/mcp.json`, `.mcp.json`, etc. |
|
|
34
|
-
| [**mcp-compliance**](https://github.com/YawLabs/mcp-compliance/blob/
|
|
34
|
+
| [**mcp-compliance**](https://github.com/YawLabs/mcp-compliance/blob/master/MCP_COMPLIANCE_SPEC.md) | Runtime testing of live MCP servers | A live server URL + transport |
|
|
35
35
|
|
|
36
36
|
The two are **complementary, not overlapping**. `mcp-config-lint` catches problems before deploy by reading JSON on disk; `mcp-compliance` catches problems after deploy by speaking the protocol to a running server. A production setup typically runs both.
|
|
37
37
|
|
|
@@ -199,7 +199,7 @@ Without this wrapper, the subprocess fails to spawn. This is the most common Win
|
|
|
199
199
|
|
|
200
200
|
## 2. Lint Rules
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
27 rules organized into 8 categories. Each rule has a unique ID, severity level, trigger condition, and message template.
|
|
203
203
|
|
|
204
204
|
Severity levels:
|
|
205
205
|
- **error** — the config is broken or has a security issue. Should fail CI.
|
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({
|
|
@@ -40512,7 +40512,10 @@ async function getCommitsSinceBatch(projectRoot, paths, since) {
|
|
|
40512
40512
|
"log",
|
|
40513
40513
|
`--since=${since.toISOString()}`,
|
|
40514
40514
|
"--name-only",
|
|
40515
|
-
|
|
40515
|
+
// %n emits a newline; prefixing with a valid format placeholder is
|
|
40516
|
+
// what keeps git from rejecting the sentinel as an invalid --pretty
|
|
40517
|
+
// format ("fatal: invalid --pretty format: ___CTXLINT_COMMIT___").
|
|
40518
|
+
`--format=%n${SENTINEL}`
|
|
40516
40519
|
]);
|
|
40517
40520
|
const normalize3 = (p2) => p2.replace(/\\/g, "/");
|
|
40518
40521
|
const requested = new Set(paths.map(normalize3));
|
|
@@ -41401,10 +41404,16 @@ function loadSettingsSources(projectRoot) {
|
|
|
41401
41404
|
path6.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
|
|
41402
41405
|
];
|
|
41403
41406
|
for (const p2 of candidates) {
|
|
41407
|
+
let content;
|
|
41404
41408
|
try {
|
|
41405
|
-
|
|
41406
|
-
sources.push(JSON.parse(content));
|
|
41409
|
+
content = fs5.readFileSync(p2, "utf-8");
|
|
41407
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}`);
|
|
41408
41417
|
}
|
|
41409
41418
|
}
|
|
41410
41419
|
return sources;
|
|
@@ -43842,7 +43851,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
43842
43851
|
import { resolve as resolve8, dirname as dirname3 } from "node:path";
|
|
43843
43852
|
import { fileURLToPath } from "node:url";
|
|
43844
43853
|
function loadVersion() {
|
|
43845
|
-
if (true) return "0.9.
|
|
43854
|
+
if (true) return "0.9.11";
|
|
43846
43855
|
const __dir = dirname3(fileURLToPath(import.meta.url));
|
|
43847
43856
|
const pkgPath = resolve8(__dir, "../package.json");
|
|
43848
43857
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
@@ -44718,7 +44727,7 @@ function applyFixes(result, options = {}) {
|
|
|
44718
44727
|
let line = lines[lineIdx];
|
|
44719
44728
|
for (const fix of lineFixes) {
|
|
44720
44729
|
if (line.includes(fix.oldText)) {
|
|
44721
|
-
line = line.
|
|
44730
|
+
line = line.replaceAll(fix.oldText, fix.newText);
|
|
44722
44731
|
totalFixes++;
|
|
44723
44732
|
const prefix = dryRun ? source_default.cyan(" Would fix") : source_default.green(" Fixed");
|
|
44724
44733
|
log(
|
|
@@ -44914,8 +44923,11 @@ var init_server3 = __esm({
|
|
|
44914
44923
|
checks: external_exports3.array(checkEnum).optional().describe("Which checks to run before fixing. Defaults to all.")
|
|
44915
44924
|
},
|
|
44916
44925
|
{
|
|
44926
|
+
// ctxlint_fix writes to disk via applyFixes() → fs.writeFileSync, so it
|
|
44927
|
+
// must advertise destructiveHint: true. Hosts (Claude Code, Cursor, etc.)
|
|
44928
|
+
// use this to decide whether to require user confirmation before the call.
|
|
44917
44929
|
readOnlyHint: false,
|
|
44918
|
-
destructiveHint:
|
|
44930
|
+
destructiveHint: true,
|
|
44919
44931
|
idempotentHint: true,
|
|
44920
44932
|
openWorldHint: false
|
|
44921
44933
|
},
|
|
@@ -51624,6 +51636,9 @@ function formatTokenReport(result) {
|
|
|
51624
51636
|
lines.push("");
|
|
51625
51637
|
return lines.join("\n");
|
|
51626
51638
|
}
|
|
51639
|
+
function isSyntheticPath(p2) {
|
|
51640
|
+
return p2.startsWith("(") || p2.startsWith("~");
|
|
51641
|
+
}
|
|
51627
51642
|
function formatSarif(result) {
|
|
51628
51643
|
const severityToLevel = {
|
|
51629
51644
|
error: "error",
|
|
@@ -51633,25 +51648,31 @@ function formatSarif(result) {
|
|
|
51633
51648
|
const results = [];
|
|
51634
51649
|
for (const file2 of result.files) {
|
|
51635
51650
|
for (const issue2 of file2.issues) {
|
|
51651
|
+
const location = isSyntheticPath(file2.path) ? {
|
|
51652
|
+
logicalLocations: [
|
|
51653
|
+
{
|
|
51654
|
+
name: file2.path,
|
|
51655
|
+
kind: "resource"
|
|
51656
|
+
}
|
|
51657
|
+
]
|
|
51658
|
+
} : {
|
|
51659
|
+
physicalLocation: {
|
|
51660
|
+
artifactLocation: {
|
|
51661
|
+
uri: file2.path,
|
|
51662
|
+
uriBaseId: "%SRCROOT%"
|
|
51663
|
+
},
|
|
51664
|
+
region: {
|
|
51665
|
+
startLine: Math.max(issue2.line, 1)
|
|
51666
|
+
}
|
|
51667
|
+
}
|
|
51668
|
+
};
|
|
51636
51669
|
const sarifResult = {
|
|
51637
51670
|
ruleId: `ctxlint/${issue2.check}`,
|
|
51638
51671
|
level: severityToLevel[issue2.severity] || "note",
|
|
51639
51672
|
message: {
|
|
51640
51673
|
text: issue2.message + (issue2.suggestion ? ` (${issue2.suggestion})` : "")
|
|
51641
51674
|
},
|
|
51642
|
-
locations: [
|
|
51643
|
-
{
|
|
51644
|
-
physicalLocation: {
|
|
51645
|
-
artifactLocation: {
|
|
51646
|
-
uri: file2.path,
|
|
51647
|
-
uriBaseId: "%SRCROOT%"
|
|
51648
|
-
},
|
|
51649
|
-
region: {
|
|
51650
|
-
startLine: Math.max(issue2.line, 1)
|
|
51651
|
-
}
|
|
51652
|
-
}
|
|
51653
|
-
}
|
|
51654
|
-
]
|
|
51675
|
+
locations: [location]
|
|
51655
51676
|
};
|
|
51656
51677
|
if (issue2.detail) {
|
|
51657
51678
|
sarifResult.message.text += `
|
|
@@ -52171,6 +52192,7 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
52171
52192
|
path11.join(resolvedPath, ".continue"),
|
|
52172
52193
|
path11.join(resolvedPath, ".vscode")
|
|
52173
52194
|
];
|
|
52195
|
+
const watchers = [];
|
|
52174
52196
|
let debounceTimer = null;
|
|
52175
52197
|
const rerun = () => {
|
|
52176
52198
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -52212,16 +52234,28 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
52212
52234
|
};
|
|
52213
52235
|
for (const filePath of watchPaths) {
|
|
52214
52236
|
try {
|
|
52215
|
-
fs9.watch(filePath, rerun);
|
|
52237
|
+
watchers.push(fs9.watch(filePath, rerun));
|
|
52216
52238
|
} catch {
|
|
52217
52239
|
}
|
|
52218
52240
|
}
|
|
52219
52241
|
for (const dir of watchDirs) {
|
|
52220
52242
|
try {
|
|
52221
|
-
fs9.watch(dir, { recursive: true }, rerun);
|
|
52243
|
+
watchers.push(fs9.watch(dir, { recursive: true }, rerun));
|
|
52222
52244
|
} catch {
|
|
52223
52245
|
}
|
|
52224
52246
|
}
|
|
52247
|
+
const shutdown = () => {
|
|
52248
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
52249
|
+
for (const w of watchers) {
|
|
52250
|
+
try {
|
|
52251
|
+
w.close();
|
|
52252
|
+
} catch {
|
|
52253
|
+
}
|
|
52254
|
+
}
|
|
52255
|
+
process.exit(0);
|
|
52256
|
+
};
|
|
52257
|
+
process.once("SIGINT", shutdown);
|
|
52258
|
+
process.once("SIGTERM", shutdown);
|
|
52225
52259
|
await new Promise(() => {
|
|
52226
52260
|
});
|
|
52227
52261
|
}
|
|
@@ -52240,11 +52274,11 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
52240
52274
|
const hookPath = path11.join(hooksDir, "pre-commit");
|
|
52241
52275
|
const fullHookContent = `#!/bin/sh
|
|
52242
52276
|
# ctxlint pre-commit hook
|
|
52243
|
-
npx @yawlabs/ctxlint --strict
|
|
52277
|
+
npx @yawlabs/ctxlint@${VERSION} --strict
|
|
52244
52278
|
`;
|
|
52245
52279
|
const appendHookContent = `
|
|
52246
52280
|
# ctxlint pre-commit hook
|
|
52247
|
-
npx @yawlabs/ctxlint --strict
|
|
52281
|
+
npx @yawlabs/ctxlint@${VERSION} --strict
|
|
52248
52282
|
`;
|
|
52249
52283
|
if (fs9.existsSync(hookPath)) {
|
|
52250
52284
|
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.11",
|
|
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
|
}
|