@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.
@@ -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
- entry: npx @yawlabs/ctxlint --strict
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
- - 5 lint rules in the `session` category with defined severities
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
 
@@ -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/main/MCP_COMPLIANCE_SPEC.md) | Runtime testing of live MCP servers | A live server URL + transport |
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
- 43 rules organized into 8 categories. Each rule has a unique ID, severity level, trigger condition, and message template.
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 ~200 KB and starts immediately
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.0
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.0
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 - 1;
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
- `--format=${SENTINEL}`
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
- const content = fs5.readFileSync(p2, "utf-8");
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.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.replace(fix.oldText, fix.newText);
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: false,
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.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.12",
106
+ "hono": "^4.12.14",
107
107
  "@hono/node-server": "^1.19.13",
108
108
  "vite": "^8.0.8"
109
109
  }