@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.
@@ -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.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
- - 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
 
@@ -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
- - 27 lint rules organized into 8 categories with defined severities
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 ~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({
@@ -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
- const content = fs5.readFileSync(p2, "utf-8");
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 > 0.6) {
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 overlap = 0;
41644
+ let intersection2 = 0;
41638
41645
  for (const line of linesA) {
41639
- if (linesB.has(line)) overlap++;
41646
+ if (linesB.has(line)) intersection2++;
41640
41647
  }
41641
- return overlap / Math.max(linesA.size, linesB.size);
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.10";
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.replace(fix.oldText, fix.newText);
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: false,
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.10",
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.12",
106
+ "hono": "^4.12.14",
107
107
  "@hono/node-server": "^1.19.13",
108
108
  "vite": "^8.0.8"
109
109
  }