@yawlabs/ctxlint 0.2.0 → 0.2.2

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 CHANGED
@@ -32,18 +32,23 @@ npx @yawlabs/ctxlint
32
32
  |------|------|
33
33
  | `CLAUDE.md`, `CLAUDE.local.md` | Claude Code |
34
34
  | `AGENTS.md` | Multi-agent |
35
- | `.cursorrules`, `.cursor/rules/*.md` | Cursor |
36
- | `copilot-instructions.md` | GitHub Copilot |
35
+ | `.cursorrules`, `.cursor/rules/*.md`, `.cursor/rules/*.mdc` | Cursor |
36
+ | `copilot-instructions.md`, `.github/copilot-instructions.md`, `.github/instructions/*.md` | GitHub Copilot |
37
37
  | `.windsurfrules`, `.windsurf/rules/*.md` | Windsurf |
38
38
  | `GEMINI.md` | Gemini |
39
39
  | `JULES.md` | Jules |
40
40
  | `.clinerules` | Cline |
41
+ | `CODEX.md` | OpenAI Codex CLI |
42
+ | `.aiderules` | Aider |
43
+ | `.aide/rules/*.md` | Aide / Codestory |
44
+ | `.amazonq/rules/*.md` | Amazon Q Developer |
45
+ | `.goose/instructions.md` | Goose by Block |
41
46
  | `CONVENTIONS.md` | General |
42
47
 
43
48
  ## Example Output
44
49
 
45
50
  ```
46
- ctxlint v0.1.0
51
+ ctxlint v0.2.1
47
52
 
48
53
  Scanning /Users/you/my-app...
49
54
 
@@ -133,11 +138,11 @@ CLI flags override config file settings.
133
138
 
134
139
  ## Use as MCP Server
135
140
 
136
- ctxlint ships with an MCP server that exposes three tools (`ctxlint_audit`, `ctxlint_validate_path`, `ctxlint_token_report`):
141
+ ctxlint ships with an MCP server that exposes four tools (`ctxlint_audit`, `ctxlint_validate_path`, `ctxlint_token_report`, `ctxlint_fix`):
137
142
 
138
143
  ```bash
139
144
  # Claude Code
140
- claude mcp add ctxlint -- node node_modules/ctxlint/dist/mcp/server.js
145
+ claude mcp add ctxlint -- node node_modules/@yawlabs/ctxlint/dist/mcp/server.js
141
146
 
142
147
  # Or run from source
143
148
  claude mcp add ctxlint -- node /path/to/ctxlint/dist/mcp/server.js
package/dist/index.js CHANGED
@@ -18,6 +18,14 @@ import { glob } from "glob";
18
18
  // src/utils/fs.ts
19
19
  import * as fs from "fs";
20
20
  import * as path from "path";
21
+ function loadPackageJson(projectRoot) {
22
+ try {
23
+ const content = fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8");
24
+ return JSON.parse(content);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
21
29
  function fileExists(filePath) {
22
30
  try {
23
31
  fs.accessSync(filePath);
@@ -93,12 +101,18 @@ var CONTEXT_FILE_PATTERNS = [
93
101
  ".cursor/rules/*.mdc",
94
102
  "copilot-instructions.md",
95
103
  ".github/copilot-instructions.md",
104
+ ".github/instructions/*.md",
96
105
  ".windsurfrules",
97
106
  ".windsurf/rules/*.md",
98
107
  "GEMINI.md",
99
108
  "JULES.md",
100
109
  ".clinerules",
101
- "CONVENTIONS.md"
110
+ "CONVENTIONS.md",
111
+ "CODEX.md",
112
+ ".aiderules",
113
+ ".aide/rules/*.md",
114
+ ".amazonq/rules/*.md",
115
+ ".goose/instructions.md"
102
116
  ];
103
117
  var IGNORED_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor"]);
104
118
  async function scanForContextFiles(projectRoot) {
@@ -554,7 +568,7 @@ function findClosestMatch(target, files) {
554
568
  // src/core/checks/commands.ts
555
569
  import * as fs3 from "fs";
556
570
  import * as path4 from "path";
557
- var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?)\s+(\S+)/;
571
+ var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
558
572
  var MAKE_PATTERN = /^make\s+(\S+)/;
559
573
  async function checkCommands(file, projectRoot) {
560
574
  const issues = [];
@@ -577,7 +591,7 @@ async function checkCommands(file, projectRoot) {
577
591
  }
578
592
  continue;
579
593
  }
580
- const shorthandMatch = cmd.match(/^(npm|pnpm|yarn)\s+(test|start|build|dev|lint|format)\b/);
594
+ const shorthandMatch = cmd.match(/^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/);
581
595
  if (shorthandMatch && pkgJson) {
582
596
  const scriptName = shorthandMatch[2];
583
597
  if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
@@ -634,14 +648,6 @@ async function checkCommands(file, projectRoot) {
634
648
  }
635
649
  return issues;
636
650
  }
637
- function loadPackageJson(projectRoot) {
638
- try {
639
- const content = fs3.readFileSync(path4.join(projectRoot, "package.json"), "utf-8");
640
- return JSON.parse(content);
641
- } catch {
642
- return null;
643
- }
644
- }
645
651
  function loadMakefile(projectRoot) {
646
652
  try {
647
653
  return fs3.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8");
@@ -764,7 +770,6 @@ function checkAggregateTokens(files) {
764
770
  }
765
771
 
766
772
  // src/core/checks/redundancy.ts
767
- import * as fs4 from "fs";
768
773
  import * as path6 from "path";
769
774
  var PACKAGE_TECH_MAP = {
770
775
  react: ["React", "react"],
@@ -819,7 +824,7 @@ var PACKAGE_TECH_MAP = {
819
824
  };
820
825
  async function checkRedundancy(file, projectRoot) {
821
826
  const issues = [];
822
- const pkgJson = loadPackageJson2(projectRoot);
827
+ const pkgJson = loadPackageJson(projectRoot);
823
828
  if (pkgJson) {
824
829
  const allDeps = /* @__PURE__ */ new Set([
825
830
  ...Object.keys(pkgJson.dependencies || {}),
@@ -914,14 +919,6 @@ function calculateLineOverlap(contentA, contentB) {
914
919
  }
915
920
  return overlap / Math.min(linesA.size, linesB.size);
916
921
  }
917
- function loadPackageJson2(projectRoot) {
918
- try {
919
- const content = fs4.readFileSync(path6.join(projectRoot, "package.json"), "utf-8");
920
- return JSON.parse(content);
921
- } catch {
922
- return null;
923
- }
924
- }
925
922
  function escapeRegex(str) {
926
923
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
927
924
  }
@@ -1026,7 +1023,7 @@ function formatIssue(issue) {
1026
1023
  }
1027
1024
 
1028
1025
  // src/core/fixer.ts
1029
- import * as fs5 from "fs";
1026
+ import * as fs4 from "fs";
1030
1027
  import chalk2 from "chalk";
1031
1028
  function applyFixes(result) {
1032
1029
  const fixesByFile = /* @__PURE__ */ new Map();
@@ -1042,7 +1039,7 @@ function applyFixes(result) {
1042
1039
  let totalFixes = 0;
1043
1040
  const filesModified = [];
1044
1041
  for (const [filePath, fixes] of fixesByFile) {
1045
- const content = fs5.readFileSync(filePath, "utf-8");
1042
+ const content = fs4.readFileSync(filePath, "utf-8");
1046
1043
  const lines = content.split("\n");
1047
1044
  let modified = false;
1048
1045
  const sortedFixes = [...fixes].sort((a, b) => b.line - a.line);
@@ -1060,7 +1057,7 @@ function applyFixes(result) {
1060
1057
  }
1061
1058
  }
1062
1059
  if (modified) {
1063
- fs5.writeFileSync(filePath, lines.join("\n"), "utf-8");
1060
+ fs4.writeFileSync(filePath, lines.join("\n"), "utf-8");
1064
1061
  filesModified.push(filePath);
1065
1062
  }
1066
1063
  }
@@ -1068,14 +1065,14 @@ function applyFixes(result) {
1068
1065
  }
1069
1066
 
1070
1067
  // src/core/config.ts
1071
- import * as fs6 from "fs";
1068
+ import * as fs5 from "fs";
1072
1069
  import * as path7 from "path";
1073
1070
  var CONFIG_FILENAMES = [".ctxlintrc", ".ctxlintrc.json"];
1074
1071
  function loadConfig(projectRoot) {
1075
1072
  for (const filename of CONFIG_FILENAMES) {
1076
1073
  const filePath = path7.join(projectRoot, filename);
1077
1074
  try {
1078
- const content = fs6.readFileSync(filePath, "utf-8");
1075
+ const content = fs5.readFileSync(filePath, "utf-8");
1079
1076
  return JSON.parse(content);
1080
1077
  } catch {
1081
1078
  continue;
@@ -1089,11 +1086,11 @@ import * as path8 from "path";
1089
1086
 
1090
1087
  // src/version.ts
1091
1088
  function loadVersion() {
1092
- if (true) return "0.2.0";
1093
- const fs7 = __require("fs");
1089
+ if (true) return "0.2.2";
1090
+ const fs6 = __require("fs");
1094
1091
  const path9 = __require("path");
1095
1092
  const pkgPath = path9.resolve(__dirname, "../package.json");
1096
- const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
1093
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
1097
1094
  return pkg.version;
1098
1095
  }
1099
1096
  var VERSION = loadVersion();
@@ -1251,30 +1248,30 @@ Fixed ${fixSummary.totalFixes} issue${fixSummary.totalFixes !== 1 ? "s" : ""} in
1251
1248
  }
1252
1249
  });
1253
1250
  program.command("init").description("Set up a git pre-commit hook that runs ctxlint --strict").action(async () => {
1254
- const fs7 = await import("fs");
1251
+ const fs6 = await import("fs");
1255
1252
  const hooksDir = path8.resolve(".git", "hooks");
1256
- if (!fs7.existsSync(path8.resolve(".git"))) {
1253
+ if (!fs6.existsSync(path8.resolve(".git"))) {
1257
1254
  console.error('Error: not a git repository. Run "git init" first.');
1258
1255
  process.exit(1);
1259
1256
  }
1260
- if (!fs7.existsSync(hooksDir)) {
1261
- fs7.mkdirSync(hooksDir, { recursive: true });
1257
+ if (!fs6.existsSync(hooksDir)) {
1258
+ fs6.mkdirSync(hooksDir, { recursive: true });
1262
1259
  }
1263
1260
  const hookPath = path8.join(hooksDir, "pre-commit");
1264
1261
  const hookContent = `#!/bin/sh
1265
1262
  # ctxlint pre-commit hook
1266
1263
  npx @yawlabs/ctxlint --strict
1267
1264
  `;
1268
- if (fs7.existsSync(hookPath)) {
1269
- const existing = fs7.readFileSync(hookPath, "utf-8");
1265
+ if (fs6.existsSync(hookPath)) {
1266
+ const existing = fs6.readFileSync(hookPath, "utf-8");
1270
1267
  if (existing.includes("ctxlint")) {
1271
1268
  console.log("Pre-commit hook already includes ctxlint.");
1272
1269
  return;
1273
1270
  }
1274
- fs7.appendFileSync(hookPath, "\n" + hookContent);
1271
+ fs6.appendFileSync(hookPath, "\n" + hookContent);
1275
1272
  console.log("Added ctxlint to existing pre-commit hook.");
1276
1273
  } else {
1277
- fs7.writeFileSync(hookPath, hookContent, { mode: 493 });
1274
+ fs6.writeFileSync(hookPath, hookContent, { mode: 493 });
1278
1275
  console.log("Created pre-commit hook at .git/hooks/pre-commit");
1279
1276
  }
1280
1277
  console.log("ctxlint will now run automatically before each commit.");
@@ -18,6 +18,14 @@ import { glob } from "glob";
18
18
  // src/utils/fs.ts
19
19
  import * as fs from "fs";
20
20
  import * as path from "path";
21
+ function loadPackageJson(projectRoot) {
22
+ try {
23
+ const content = fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8");
24
+ return JSON.parse(content);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
21
29
  function fileExists(filePath) {
22
30
  try {
23
31
  fs.accessSync(filePath);
@@ -93,12 +101,18 @@ var CONTEXT_FILE_PATTERNS = [
93
101
  ".cursor/rules/*.mdc",
94
102
  "copilot-instructions.md",
95
103
  ".github/copilot-instructions.md",
104
+ ".github/instructions/*.md",
96
105
  ".windsurfrules",
97
106
  ".windsurf/rules/*.md",
98
107
  "GEMINI.md",
99
108
  "JULES.md",
100
109
  ".clinerules",
101
- "CONVENTIONS.md"
110
+ "CONVENTIONS.md",
111
+ "CODEX.md",
112
+ ".aiderules",
113
+ ".aide/rules/*.md",
114
+ ".amazonq/rules/*.md",
115
+ ".goose/instructions.md"
102
116
  ];
103
117
  var IGNORED_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor"]);
104
118
  async function scanForContextFiles(projectRoot) {
@@ -551,7 +565,7 @@ function findClosestMatch(target, files) {
551
565
  // src/core/checks/commands.ts
552
566
  import * as fs3 from "fs";
553
567
  import * as path4 from "path";
554
- var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?)\s+(\S+)/;
568
+ var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
555
569
  var MAKE_PATTERN = /^make\s+(\S+)/;
556
570
  async function checkCommands(file, projectRoot) {
557
571
  const issues = [];
@@ -574,7 +588,7 @@ async function checkCommands(file, projectRoot) {
574
588
  }
575
589
  continue;
576
590
  }
577
- const shorthandMatch = cmd.match(/^(npm|pnpm|yarn)\s+(test|start|build|dev|lint|format)\b/);
591
+ const shorthandMatch = cmd.match(/^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/);
578
592
  if (shorthandMatch && pkgJson) {
579
593
  const scriptName = shorthandMatch[2];
580
594
  if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
@@ -631,14 +645,6 @@ async function checkCommands(file, projectRoot) {
631
645
  }
632
646
  return issues;
633
647
  }
634
- function loadPackageJson(projectRoot) {
635
- try {
636
- const content = fs3.readFileSync(path4.join(projectRoot, "package.json"), "utf-8");
637
- return JSON.parse(content);
638
- } catch {
639
- return null;
640
- }
641
- }
642
648
  function loadMakefile(projectRoot) {
643
649
  try {
644
650
  return fs3.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8");
@@ -755,7 +761,6 @@ function checkAggregateTokens(files) {
755
761
  }
756
762
 
757
763
  // src/core/checks/redundancy.ts
758
- import * as fs4 from "fs";
759
764
  import * as path6 from "path";
760
765
  var PACKAGE_TECH_MAP = {
761
766
  react: ["React", "react"],
@@ -810,7 +815,7 @@ var PACKAGE_TECH_MAP = {
810
815
  };
811
816
  async function checkRedundancy(file, projectRoot) {
812
817
  const issues = [];
813
- const pkgJson = loadPackageJson2(projectRoot);
818
+ const pkgJson = loadPackageJson(projectRoot);
814
819
  if (pkgJson) {
815
820
  const allDeps = /* @__PURE__ */ new Set([
816
821
  ...Object.keys(pkgJson.dependencies || {}),
@@ -905,24 +910,58 @@ function calculateLineOverlap(contentA, contentB) {
905
910
  }
906
911
  return overlap / Math.min(linesA.size, linesB.size);
907
912
  }
908
- function loadPackageJson2(projectRoot) {
909
- try {
910
- const content = fs4.readFileSync(path6.join(projectRoot, "package.json"), "utf-8");
911
- return JSON.parse(content);
912
- } catch {
913
- return null;
914
- }
915
- }
916
913
  function escapeRegex(str) {
917
914
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
918
915
  }
919
916
 
917
+ // src/core/fixer.ts
918
+ import * as fs4 from "fs";
919
+ import chalk from "chalk";
920
+ function applyFixes(result) {
921
+ const fixesByFile = /* @__PURE__ */ new Map();
922
+ for (const file of result.files) {
923
+ for (const issue of file.issues) {
924
+ if (issue.fix) {
925
+ const existing = fixesByFile.get(issue.fix.file) || [];
926
+ existing.push(issue.fix);
927
+ fixesByFile.set(issue.fix.file, existing);
928
+ }
929
+ }
930
+ }
931
+ let totalFixes = 0;
932
+ const filesModified = [];
933
+ for (const [filePath, fixes] of fixesByFile) {
934
+ const content = fs4.readFileSync(filePath, "utf-8");
935
+ const lines = content.split("\n");
936
+ let modified = false;
937
+ const sortedFixes = [...fixes].sort((a, b) => b.line - a.line);
938
+ for (const fix of sortedFixes) {
939
+ const lineIdx = fix.line - 1;
940
+ if (lineIdx < 0 || lineIdx >= lines.length) continue;
941
+ const line = lines[lineIdx];
942
+ if (line.includes(fix.oldText)) {
943
+ lines[lineIdx] = line.replace(fix.oldText, fix.newText);
944
+ modified = true;
945
+ totalFixes++;
946
+ console.log(
947
+ chalk.green(" Fixed") + ` Line ${fix.line}: ${chalk.dim(fix.oldText)} ${chalk.dim("\u2192")} ${fix.newText}`
948
+ );
949
+ }
950
+ }
951
+ if (modified) {
952
+ fs4.writeFileSync(filePath, lines.join("\n"), "utf-8");
953
+ filesModified.push(filePath);
954
+ }
955
+ }
956
+ return { totalFixes, filesModified };
957
+ }
958
+
920
959
  // src/mcp/server.ts
921
960
  import * as path7 from "path";
922
961
 
923
962
  // src/version.ts
924
963
  function loadVersion() {
925
- if (true) return "0.2.0";
964
+ if (true) return "0.2.2";
926
965
  const fs5 = __require("fs");
927
966
  const path8 = __require("path");
928
967
  const pkgPath = path8.resolve(__dirname, "../package.json");
@@ -1035,6 +1074,47 @@ server.tool(
1035
1074
  }
1036
1075
  }
1037
1076
  );
1077
+ server.tool(
1078
+ "ctxlint_fix",
1079
+ "Run the linter with --fix mode to auto-correct broken file paths in context files using git history and fuzzy matching. Returns a summary of what was fixed.",
1080
+ {
1081
+ projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
1082
+ checks: z.array(z.enum(["paths", "commands", "staleness", "tokens", "redundancy"])).optional().describe("Which checks to run before fixing. Defaults to all.")
1083
+ },
1084
+ async ({ projectPath, checks }) => {
1085
+ const root = path7.resolve(projectPath || process.cwd());
1086
+ const activeChecks = checks || ALL_CHECKS;
1087
+ try {
1088
+ const result = await runAudit(root, activeChecks);
1089
+ const fixSummary = applyFixes(result);
1090
+ return {
1091
+ content: [
1092
+ {
1093
+ type: "text",
1094
+ text: JSON.stringify(
1095
+ {
1096
+ totalFixes: fixSummary.totalFixes,
1097
+ filesModified: fixSummary.filesModified,
1098
+ remainingIssues: result.summary
1099
+ },
1100
+ null,
1101
+ 2
1102
+ )
1103
+ }
1104
+ ]
1105
+ };
1106
+ } catch (err) {
1107
+ const msg = err instanceof Error ? err.message : String(err);
1108
+ return {
1109
+ content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
1110
+ isError: true
1111
+ };
1112
+ } finally {
1113
+ freeEncoder();
1114
+ resetGit();
1115
+ }
1116
+ }
1117
+ );
1038
1118
  async function runAudit(projectRoot, activeChecks) {
1039
1119
  const discovered = await scanForContextFiles(projectRoot);
1040
1120
  const parsed = discovered.map((f) => parseContextFile(f));
package/package.json CHANGED
@@ -1,20 +1,11 @@
1
1
  {
2
2
  "name": "@yawlabs/ctxlint",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Lint your AI agent context files (CLAUDE.md, AGENTS.md, etc.) against your actual codebase",
5
5
  "bin": {
6
6
  "ctxlint": "dist/index.js"
7
7
  },
8
8
  "type": "module",
9
- "scripts": {
10
- "build": "tsup",
11
- "dev": "tsup --watch",
12
- "test": "vitest",
13
- "test:run": "vitest run",
14
- "lint": "eslint src/",
15
- "format": "prettier --write src/",
16
- "mcp": "node dist/mcp/server.js"
17
- },
18
9
  "keywords": [
19
10
  "claude",
20
11
  "agents",
@@ -41,7 +32,6 @@
41
32
  "engines": {
42
33
  "node": ">=20"
43
34
  },
44
- "packageManager": "pnpm@10.33.0",
45
35
  "dependencies": {
46
36
  "@modelcontextprotocol/sdk": "^1.29.0",
47
37
  "chalk": "^5.6.2",
@@ -64,9 +54,13 @@
64
54
  "typescript-eslint": "^8.58.0",
65
55
  "vitest": "^4.1.2"
66
56
  },
67
- "pnpm": {
68
- "onlyBuiltDependencies": [
69
- "esbuild"
70
- ]
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "dev": "tsup --watch",
60
+ "test": "vitest",
61
+ "test:run": "vitest run",
62
+ "lint": "eslint src/",
63
+ "format": "prettier --write src/",
64
+ "mcp": "node dist/mcp/server.js"
71
65
  }
72
- }
66
+ }