@yawlabs/ctxlint 0.3.0 → 0.5.0

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.
@@ -0,0 +1,2550 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/utils/fs.ts
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ function loadPackageJson(projectRoot) {
13
+ try {
14
+ const content = fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8");
15
+ return JSON.parse(content);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function fileExists(filePath) {
21
+ try {
22
+ fs.accessSync(filePath);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ function isDirectory(filePath) {
29
+ try {
30
+ return fs.statSync(filePath).isDirectory();
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+ function isSymlink(filePath) {
36
+ try {
37
+ return fs.lstatSync(filePath).isSymbolicLink();
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ function readSymlinkTarget(filePath) {
43
+ try {
44
+ return fs.readlinkSync(filePath);
45
+ } catch {
46
+ return void 0;
47
+ }
48
+ }
49
+ function readFileContent(filePath) {
50
+ return fs.readFileSync(filePath, "utf-8");
51
+ }
52
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
53
+ "node_modules",
54
+ ".git",
55
+ "dist",
56
+ "build",
57
+ "vendor",
58
+ ".next",
59
+ ".nuxt",
60
+ "coverage",
61
+ "__pycache__"
62
+ ]);
63
+ function getAllProjectFiles(projectRoot) {
64
+ const files = [];
65
+ function walk(dir, depth) {
66
+ if (depth > 10) return;
67
+ try {
68
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
69
+ for (const entry of entries) {
70
+ if (IGNORED_DIRS.has(entry.name)) continue;
71
+ const fullPath = path.join(dir, entry.name);
72
+ if (entry.isDirectory()) {
73
+ walk(fullPath, depth + 1);
74
+ } else {
75
+ files.push(path.relative(projectRoot, fullPath));
76
+ }
77
+ }
78
+ } catch {
79
+ }
80
+ }
81
+ walk(projectRoot, 0);
82
+ return files;
83
+ }
84
+
85
+ // src/core/scanner.ts
86
+ import * as fs2 from "fs";
87
+ import * as path2 from "path";
88
+ import { glob } from "glob";
89
+ var CONTEXT_FILE_PATTERNS = [
90
+ // Claude Code
91
+ "CLAUDE.md",
92
+ "CLAUDE.local.md",
93
+ ".claude/rules/*.md",
94
+ // AGENTS.md (AAIF / Linux Foundation standard)
95
+ "AGENTS.md",
96
+ "AGENT.md",
97
+ "AGENTS.override.md",
98
+ // Cursor
99
+ ".cursorrules",
100
+ ".cursor/rules/*.md",
101
+ ".cursor/rules/*.mdc",
102
+ ".cursor/rules/*/RULE.md",
103
+ // GitHub Copilot
104
+ ".github/copilot-instructions.md",
105
+ ".github/instructions/*.md",
106
+ ".github/git-commit-instructions.md",
107
+ // Windsurf
108
+ ".windsurfrules",
109
+ ".windsurf/rules/*.md",
110
+ // Gemini CLI
111
+ "GEMINI.md",
112
+ // Cline
113
+ ".clinerules",
114
+ // Aider — note: .aiderules has no file extension; this is the intended format
115
+ ".aiderules",
116
+ // Aide / Codestory
117
+ ".aide/rules/*.md",
118
+ // Amazon Q Developer
119
+ ".amazonq/rules/*.md",
120
+ // Goose (Block)
121
+ ".goose/instructions.md",
122
+ ".goosehints",
123
+ // JetBrains Junie
124
+ ".junie/guidelines.md",
125
+ ".junie/AGENTS.md",
126
+ // JetBrains AI Assistant
127
+ ".aiassistant/rules/*.md",
128
+ // Continue
129
+ ".continuerules",
130
+ ".continue/rules/*.md",
131
+ // Zed
132
+ ".rules",
133
+ // Replit
134
+ "replit.md"
135
+ ];
136
+ var IGNORED_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor"]);
137
+ async function scanForContextFiles(projectRoot, options = {}) {
138
+ const maxDepth = options.depth ?? 2;
139
+ const patterns = [...CONTEXT_FILE_PATTERNS, ...options.extraPatterns || []];
140
+ const found = [];
141
+ const seen = /* @__PURE__ */ new Set();
142
+ const dirsToScan = [projectRoot];
143
+ function collectDirs(dir, currentDepth) {
144
+ if (currentDepth >= maxDepth) return;
145
+ try {
146
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
147
+ for (const entry of entries) {
148
+ if (entry.isDirectory() && !IGNORED_DIRS2.has(entry.name) && !entry.name.startsWith(".")) {
149
+ const fullPath = path2.join(dir, entry.name);
150
+ dirsToScan.push(fullPath);
151
+ collectDirs(fullPath, currentDepth + 1);
152
+ }
153
+ }
154
+ } catch {
155
+ }
156
+ }
157
+ collectDirs(projectRoot, 0);
158
+ for (const dir of dirsToScan) {
159
+ for (const pattern of patterns) {
160
+ const matches = await glob(pattern, {
161
+ cwd: dir,
162
+ absolute: true,
163
+ nodir: true,
164
+ dot: true
165
+ });
166
+ for (const match of matches) {
167
+ const normalized = path2.normalize(match);
168
+ if (seen.has(normalized)) continue;
169
+ seen.add(normalized);
170
+ const relativePath = path2.relative(projectRoot, normalized);
171
+ const symlink = isSymlink(normalized);
172
+ const target = symlink ? readSymlinkTarget(normalized) : void 0;
173
+ found.push({
174
+ absolutePath: normalized,
175
+ relativePath: relativePath.replace(/\\/g, "/"),
176
+ isSymlink: symlink,
177
+ symlinkTarget: target,
178
+ type: "context"
179
+ });
180
+ }
181
+ }
182
+ }
183
+ return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
184
+ }
185
+ var MCP_CONFIG_PATTERNS = [
186
+ ".mcp.json",
187
+ ".cursor/mcp.json",
188
+ ".vscode/mcp.json",
189
+ ".amazonq/mcp.json",
190
+ ".continue/mcpServers/*.json"
191
+ ];
192
+ async function scanForMcpConfigs(projectRoot) {
193
+ const found = [];
194
+ const seen = /* @__PURE__ */ new Set();
195
+ for (const pattern of MCP_CONFIG_PATTERNS) {
196
+ const matches = await glob(pattern, {
197
+ cwd: projectRoot,
198
+ absolute: true,
199
+ nodir: true,
200
+ dot: true
201
+ });
202
+ for (const match of matches) {
203
+ const normalized = path2.normalize(match);
204
+ if (seen.has(normalized)) continue;
205
+ seen.add(normalized);
206
+ const relativePath = path2.relative(projectRoot, normalized);
207
+ const symlink = isSymlink(normalized);
208
+ const target = symlink ? readSymlinkTarget(normalized) : void 0;
209
+ found.push({
210
+ absolutePath: normalized,
211
+ relativePath: relativePath.replace(/\\/g, "/"),
212
+ isSymlink: symlink,
213
+ symlinkTarget: target,
214
+ type: "mcp-config"
215
+ });
216
+ }
217
+ }
218
+ return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
219
+ }
220
+ async function scanGlobalMcpConfigs() {
221
+ const found = [];
222
+ const seen = /* @__PURE__ */ new Set();
223
+ const home = process.env.HOME || process.env.USERPROFILE || "";
224
+ const globalPaths = [
225
+ path2.join(home, ".claude.json"),
226
+ path2.join(home, ".claude", "settings.json"),
227
+ path2.join(home, ".cursor", "mcp.json"),
228
+ path2.join(home, ".codeium", "windsurf", "mcp_config.json"),
229
+ path2.join(home, ".aws", "amazonq", "mcp.json")
230
+ ];
231
+ if (process.platform === "darwin") {
232
+ globalPaths.push(
233
+ path2.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
234
+ );
235
+ } else if (process.platform === "win32") {
236
+ const appData = process.env.APPDATA || path2.join(home, "AppData", "Roaming");
237
+ globalPaths.push(path2.join(appData, "Claude", "claude_desktop_config.json"));
238
+ }
239
+ for (const filePath of globalPaths) {
240
+ const normalized = path2.normalize(filePath);
241
+ if (seen.has(normalized)) continue;
242
+ seen.add(normalized);
243
+ try {
244
+ fs2.accessSync(normalized);
245
+ } catch {
246
+ continue;
247
+ }
248
+ const symlink = isSymlink(normalized);
249
+ const target = symlink ? readSymlinkTarget(normalized) : void 0;
250
+ found.push({
251
+ absolutePath: normalized,
252
+ relativePath: "~/" + path2.relative(home, normalized).replace(/\\/g, "/"),
253
+ isSymlink: symlink,
254
+ symlinkTarget: target,
255
+ type: "mcp-config"
256
+ });
257
+ }
258
+ return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
259
+ }
260
+
261
+ // src/utils/tokens.ts
262
+ import { encoding_for_model } from "tiktoken";
263
+ var encoder = null;
264
+ function getEncoder() {
265
+ if (!encoder) {
266
+ encoder = encoding_for_model("gpt-4");
267
+ }
268
+ return encoder;
269
+ }
270
+ function countTokens(text) {
271
+ try {
272
+ const enc = getEncoder();
273
+ const tokens = enc.encode(text);
274
+ return tokens.length;
275
+ } catch {
276
+ return Math.ceil(text.length / 4);
277
+ }
278
+ }
279
+ function freeEncoder() {
280
+ if (encoder) {
281
+ encoder.free();
282
+ encoder = null;
283
+ }
284
+ }
285
+
286
+ // src/core/parser.ts
287
+ var PATH_PATTERN = /(?:^|[\s`"'(])((\.{0,2}\/)?(?:[\w@.-]+\/)+[\w.*-]+(?:\.\w+)?)(?=[\s`"'),;:]|$)/gm;
288
+ var PATH_EXCLUDE = /^(https?:\/\/|ftp:\/\/|mailto:|n\/a|w\/o|I\/O|i\/o|e\.g\.|N\/A|\.deb\/|\.rpm[.\/]|\.tar[.\/]|\.zip[.\/])/i;
289
+ var COMMAND_PREFIXES = /^\s*[\$>]\s+(.+)$/;
290
+ var COMMON_COMMANDS = /^(npm\s+run|npx|pnpm|yarn|make|cargo|go\s+(run|build|test)|python|pytest|vitest|jest|bun|deno)\b/;
291
+ function parseContextFile(file) {
292
+ const content = readFileContent(file.absolutePath);
293
+ const lines = content.split("\n");
294
+ const sections = parseSections(lines);
295
+ const paths = extractPathReferences(lines, sections);
296
+ const commands = extractCommandReferences(lines, sections);
297
+ return {
298
+ filePath: file.absolutePath,
299
+ relativePath: file.relativePath,
300
+ isSymlink: file.isSymlink,
301
+ symlinkTarget: file.symlinkTarget,
302
+ totalTokens: countTokens(content),
303
+ totalLines: lines.length,
304
+ content,
305
+ sections,
306
+ references: {
307
+ paths,
308
+ commands
309
+ }
310
+ };
311
+ }
312
+ function parseSections(lines) {
313
+ const sections = [];
314
+ for (let i = 0; i < lines.length; i++) {
315
+ const match = lines[i].match(/^(#{1,6})\s+(.+)/);
316
+ if (match) {
317
+ if (sections.length > 0) {
318
+ const prev = sections[sections.length - 1];
319
+ if (prev.endLine === -1) {
320
+ prev.endLine = i - 1;
321
+ }
322
+ }
323
+ sections.push({
324
+ title: match[2].trim(),
325
+ startLine: i + 1,
326
+ // 1-indexed
327
+ endLine: -1,
328
+ level: match[1].length
329
+ });
330
+ }
331
+ }
332
+ if (sections.length > 0) {
333
+ const last = sections[sections.length - 1];
334
+ if (last.endLine === -1) {
335
+ last.endLine = lines.length;
336
+ }
337
+ }
338
+ return sections;
339
+ }
340
+ function getSectionForLine(line, sections) {
341
+ for (let i = sections.length - 1; i >= 0; i--) {
342
+ if (line >= sections[i].startLine) {
343
+ return sections[i].title;
344
+ }
345
+ }
346
+ return void 0;
347
+ }
348
+ function extractPathReferences(lines, sections) {
349
+ const paths = [];
350
+ let inCodeBlock = false;
351
+ let codeBlockLang = "";
352
+ for (let i = 0; i < lines.length; i++) {
353
+ const line = lines[i];
354
+ if (line.trimStart().startsWith("```")) {
355
+ if (!inCodeBlock) {
356
+ inCodeBlock = true;
357
+ codeBlockLang = line.trimStart().slice(3).trim().toLowerCase();
358
+ } else {
359
+ inCodeBlock = false;
360
+ codeBlockLang = "";
361
+ }
362
+ continue;
363
+ }
364
+ if (inCodeBlock && isExampleCodeBlock(codeBlockLang)) {
365
+ continue;
366
+ }
367
+ PATH_PATTERN.lastIndex = 0;
368
+ let match;
369
+ while ((match = PATH_PATTERN.exec(line)) !== null) {
370
+ const value = match[1];
371
+ if (PATH_EXCLUDE.test(value)) continue;
372
+ if (value.length < 3) continue;
373
+ if (/^v?\d+\.\d+\//.test(value)) continue;
374
+ const column = match.index + match[0].length - match[1].length + 1;
375
+ paths.push({
376
+ value,
377
+ line: i + 1,
378
+ // 1-indexed
379
+ column,
380
+ section: getSectionForLine(i + 1, sections)
381
+ });
382
+ }
383
+ }
384
+ return paths;
385
+ }
386
+ function isExampleCodeBlock(lang) {
387
+ return [
388
+ "javascript",
389
+ "js",
390
+ "typescript",
391
+ "ts",
392
+ "python",
393
+ "py",
394
+ "go",
395
+ "rust",
396
+ "java",
397
+ "c",
398
+ "cpp",
399
+ "ruby",
400
+ "php",
401
+ "json",
402
+ "yaml",
403
+ "yml",
404
+ "toml",
405
+ "xml",
406
+ "html",
407
+ "css",
408
+ "sql",
409
+ "graphql",
410
+ "jsx",
411
+ "tsx"
412
+ ].includes(lang);
413
+ }
414
+ function extractCommandReferences(lines, sections) {
415
+ const commands = [];
416
+ let inCodeBlock = false;
417
+ let codeBlockLang = "";
418
+ for (let i = 0; i < lines.length; i++) {
419
+ const line = lines[i];
420
+ if (line.trimStart().startsWith("```")) {
421
+ if (!inCodeBlock) {
422
+ inCodeBlock = true;
423
+ codeBlockLang = line.trimStart().slice(3).trim().toLowerCase();
424
+ } else {
425
+ inCodeBlock = false;
426
+ codeBlockLang = "";
427
+ }
428
+ continue;
429
+ }
430
+ const prefixMatch = line.match(COMMAND_PREFIXES);
431
+ if (prefixMatch) {
432
+ commands.push({
433
+ value: prefixMatch[1].trim(),
434
+ line: i + 1,
435
+ column: prefixMatch.index + prefixMatch[0].length - prefixMatch[1].length + 1,
436
+ section: getSectionForLine(i + 1, sections)
437
+ });
438
+ continue;
439
+ }
440
+ if (inCodeBlock && ["bash", "sh", "shell", "zsh", ""].includes(codeBlockLang)) {
441
+ const trimmed = line.trim();
442
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("//")) {
443
+ if (COMMON_COMMANDS.test(trimmed) || trimmed.startsWith("$") || trimmed.startsWith(">")) {
444
+ const cmd = trimmed.replace(/^\s*[\$>]\s*/, "");
445
+ if (cmd) {
446
+ const cmdStart = line.indexOf(trimmed) + trimmed.indexOf(cmd);
447
+ commands.push({
448
+ value: cmd,
449
+ line: i + 1,
450
+ column: cmdStart + 1,
451
+ section: getSectionForLine(i + 1, sections)
452
+ });
453
+ }
454
+ }
455
+ }
456
+ continue;
457
+ }
458
+ const inlineMatches = line.matchAll(/`([^`]+)`/g);
459
+ for (const m of inlineMatches) {
460
+ const cmd = m[1].trim();
461
+ if (COMMON_COMMANDS.test(cmd)) {
462
+ commands.push({
463
+ value: cmd,
464
+ line: i + 1,
465
+ column: (m.index ?? 0) + 2,
466
+ section: getSectionForLine(i + 1, sections)
467
+ });
468
+ }
469
+ }
470
+ }
471
+ return commands;
472
+ }
473
+
474
+ // src/utils/git.ts
475
+ import simpleGit from "simple-git";
476
+ var gitInstance = null;
477
+ var gitProjectRoot = null;
478
+ function getGit(projectRoot) {
479
+ if (!gitInstance || gitProjectRoot !== projectRoot) {
480
+ gitInstance = simpleGit(projectRoot);
481
+ gitProjectRoot = projectRoot;
482
+ }
483
+ return gitInstance;
484
+ }
485
+ function resetGit() {
486
+ gitInstance = null;
487
+ gitProjectRoot = null;
488
+ }
489
+ async function isGitRepo(projectRoot) {
490
+ try {
491
+ const git = simpleGit(projectRoot);
492
+ await git.revparse(["--is-inside-work-tree"]);
493
+ return true;
494
+ } catch {
495
+ return false;
496
+ }
497
+ }
498
+ async function getFileLastModified(projectRoot, filePath) {
499
+ try {
500
+ const git = getGit(projectRoot);
501
+ const log = await git.log({ file: filePath, maxCount: 1 });
502
+ if (log.latest?.date) {
503
+ return new Date(log.latest.date);
504
+ }
505
+ return null;
506
+ } catch {
507
+ return null;
508
+ }
509
+ }
510
+ async function getCommitsSince(projectRoot, filePath, since) {
511
+ try {
512
+ const git = getGit(projectRoot);
513
+ const log = await git.log({
514
+ file: filePath,
515
+ "--since": since.toISOString()
516
+ });
517
+ return log.total;
518
+ } catch {
519
+ return 0;
520
+ }
521
+ }
522
+ async function findRenames(projectRoot, filePath) {
523
+ try {
524
+ const git = getGit(projectRoot);
525
+ const result = await git.raw([
526
+ "log",
527
+ "--diff-filter=R",
528
+ "--find-renames",
529
+ "--name-status",
530
+ "--format=%H %ai",
531
+ "-10",
532
+ "--",
533
+ filePath
534
+ ]);
535
+ if (!result.trim()) return null;
536
+ const lines = result.trim().split("\n");
537
+ for (let i = 0; i < lines.length; i++) {
538
+ const line = lines[i];
539
+ if (line.startsWith("R")) {
540
+ const parts = line.split(" ");
541
+ if (parts.length >= 3) {
542
+ const hashLine = lines[i - 1] || "";
543
+ const hashMatch = hashLine.match(/^([a-f0-9]+)\s+(.+)/);
544
+ const commitHash = hashMatch?.[1]?.substring(0, 7) || "unknown";
545
+ const dateStr = hashMatch?.[2];
546
+ const daysAgo = dateStr ? Math.floor((Date.now() - new Date(dateStr).getTime()) / (1e3 * 60 * 60 * 24)) : 0;
547
+ return {
548
+ oldPath: parts[1],
549
+ newPath: parts[2],
550
+ commitHash,
551
+ daysAgo
552
+ };
553
+ }
554
+ }
555
+ }
556
+ return null;
557
+ } catch {
558
+ return null;
559
+ }
560
+ }
561
+
562
+ // src/core/checks/paths.ts
563
+ import * as path3 from "path";
564
+ import levenshteinPkg from "fast-levenshtein";
565
+ import { glob as glob2 } from "glob";
566
+ var levenshtein = levenshteinPkg.get;
567
+ var cachedProjectFiles = null;
568
+ function getProjectFiles(projectRoot) {
569
+ if (cachedProjectFiles?.root === projectRoot) return cachedProjectFiles.files;
570
+ const files = getAllProjectFiles(projectRoot);
571
+ cachedProjectFiles = { root: projectRoot, files };
572
+ return files;
573
+ }
574
+ function resetPathsCache() {
575
+ cachedProjectFiles = null;
576
+ }
577
+ async function checkPaths(file, projectRoot) {
578
+ const issues = [];
579
+ const projectFiles = getProjectFiles(projectRoot);
580
+ const contextDir = path3.dirname(file.filePath);
581
+ for (const ref of file.references.paths) {
582
+ const baseDir = ref.value.startsWith("./") || ref.value.startsWith("../") ? contextDir : projectRoot;
583
+ const resolvedPath = path3.resolve(baseDir, ref.value);
584
+ const normalizedRef = ref.value.replace(/\\/g, "/");
585
+ if (normalizedRef.includes("*")) {
586
+ const matches = await glob2(normalizedRef, { cwd: baseDir, nodir: false });
587
+ if (matches.length === 0) {
588
+ issues.push({
589
+ severity: "error",
590
+ check: "paths",
591
+ line: ref.line,
592
+ message: `${ref.value} matches no files`,
593
+ suggestion: "Verify the glob pattern is correct"
594
+ });
595
+ }
596
+ continue;
597
+ }
598
+ const isDir = normalizedRef.endsWith("/");
599
+ if (isDir) {
600
+ const dirPath = path3.resolve(baseDir, normalizedRef);
601
+ if (!isDirectory(dirPath)) {
602
+ issues.push({
603
+ severity: "error",
604
+ check: "paths",
605
+ line: ref.line,
606
+ message: `${ref.value} directory does not exist`
607
+ });
608
+ }
609
+ continue;
610
+ }
611
+ if (fileExists(resolvedPath) || isDirectory(resolvedPath)) {
612
+ continue;
613
+ }
614
+ let suggestion;
615
+ let detail;
616
+ let fixTarget;
617
+ const rename = await findRenames(projectRoot, ref.value);
618
+ if (rename) {
619
+ fixTarget = rename.newPath;
620
+ suggestion = `Did you mean ${rename.newPath}?`;
621
+ detail = `Renamed ${rename.daysAgo} days ago in commit ${rename.commitHash}`;
622
+ } else {
623
+ const match = findClosestMatch(normalizedRef, projectFiles);
624
+ if (match) {
625
+ fixTarget = match;
626
+ suggestion = `Did you mean ${match}?`;
627
+ }
628
+ }
629
+ issues.push({
630
+ severity: "error",
631
+ check: "paths",
632
+ line: ref.line,
633
+ message: `${ref.value} does not exist`,
634
+ suggestion,
635
+ detail,
636
+ fix: fixTarget ? { file: file.filePath, line: ref.line, oldText: ref.value, newText: fixTarget } : void 0
637
+ });
638
+ }
639
+ return issues;
640
+ }
641
+ function findClosestMatch(target, files) {
642
+ const targetNorm = target.replace(/\\/g, "/");
643
+ const targetBase = path3.basename(targetNorm);
644
+ let bestMatch = null;
645
+ let bestDistance = Infinity;
646
+ for (const file of files) {
647
+ const fileNorm = file.replace(/\\/g, "/");
648
+ if (path3.basename(fileNorm) === targetBase && fileNorm !== targetNorm) {
649
+ const dist = levenshtein(targetNorm, fileNorm);
650
+ if (dist < bestDistance) {
651
+ bestDistance = dist;
652
+ bestMatch = fileNorm;
653
+ }
654
+ }
655
+ }
656
+ if (!bestMatch) {
657
+ for (const file of files) {
658
+ const fileNorm = file.replace(/\\/g, "/");
659
+ const dist = levenshtein(targetNorm, fileNorm);
660
+ if (dist < bestDistance && dist <= Math.max(targetNorm.length * 0.4, 5)) {
661
+ bestDistance = dist;
662
+ bestMatch = fileNorm;
663
+ }
664
+ }
665
+ }
666
+ return bestMatch;
667
+ }
668
+
669
+ // src/core/checks/tokens.ts
670
+ var DEFAULT_THRESHOLDS = {
671
+ info: 1e3,
672
+ warning: 3e3,
673
+ error: 8e3,
674
+ aggregate: 5e3
675
+ };
676
+ var currentThresholds = DEFAULT_THRESHOLDS;
677
+ function setTokenThresholds(overrides) {
678
+ currentThresholds = { ...DEFAULT_THRESHOLDS, ...overrides };
679
+ }
680
+ function resetTokenThresholds() {
681
+ currentThresholds = DEFAULT_THRESHOLDS;
682
+ }
683
+ async function checkTokens(file, _projectRoot) {
684
+ const issues = [];
685
+ const tokens = file.totalTokens;
686
+ if (tokens >= currentThresholds.error) {
687
+ issues.push({
688
+ severity: "error",
689
+ check: "tokens",
690
+ line: 1,
691
+ message: `${tokens.toLocaleString()} tokens \u2014 consumes significant context window space`,
692
+ suggestion: "Consider splitting into focused sections or removing redundant content."
693
+ });
694
+ } else if (tokens >= currentThresholds.warning) {
695
+ issues.push({
696
+ severity: "warning",
697
+ check: "tokens",
698
+ line: 1,
699
+ message: `${tokens.toLocaleString()} tokens \u2014 large context file`,
700
+ suggestion: "Consider trimming \u2014 research shows diminishing returns past ~300 lines."
701
+ });
702
+ } else if (tokens >= currentThresholds.info) {
703
+ issues.push({
704
+ severity: "info",
705
+ check: "tokens",
706
+ line: 1,
707
+ message: `Uses ~${tokens.toLocaleString()} tokens per session`
708
+ });
709
+ }
710
+ return issues;
711
+ }
712
+ function checkAggregateTokens(files) {
713
+ const total = files.reduce((sum, f) => sum + f.tokens, 0);
714
+ if (total > currentThresholds.aggregate && files.length > 1) {
715
+ return {
716
+ severity: "warning",
717
+ check: "tokens",
718
+ line: 0,
719
+ message: `${files.length} context files consume ${total.toLocaleString()} tokens combined`,
720
+ suggestion: "Consider consolidating or trimming to reduce per-session context cost."
721
+ };
722
+ }
723
+ return null;
724
+ }
725
+
726
+ // src/version.ts
727
+ function loadVersion() {
728
+ if (true) return "0.5.0";
729
+ const fs6 = __require("fs");
730
+ const path8 = __require("path");
731
+ const pkgPath = path8.resolve(__dirname, "../package.json");
732
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
733
+ return pkg.version;
734
+ }
735
+ var VERSION = loadVersion();
736
+
737
+ // src/core/mcp-parser.ts
738
+ import { execSync } from "child_process";
739
+ function parseMcpConfig(file, projectRoot, scopeOverride) {
740
+ const content = readFileContent(file.absolutePath);
741
+ const client = detectClient(file.relativePath);
742
+ const scope = scopeOverride ?? detectScope(file.relativePath);
743
+ const expectedRootKey = client === "vscode" ? "servers" : "mcpServers";
744
+ const isGitTracked = checkGitTracked(file.absolutePath, projectRoot);
745
+ const result = {
746
+ filePath: file.absolutePath,
747
+ relativePath: file.relativePath,
748
+ client,
749
+ scope,
750
+ expectedRootKey,
751
+ actualRootKey: null,
752
+ servers: [],
753
+ parseErrors: [],
754
+ content,
755
+ isGitTracked
756
+ };
757
+ let parsed;
758
+ try {
759
+ parsed = JSON.parse(content);
760
+ } catch (err) {
761
+ const message = err instanceof Error ? err.message : String(err);
762
+ result.parseErrors.push(message);
763
+ return result;
764
+ }
765
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
766
+ result.parseErrors.push("MCP config must be a JSON object");
767
+ return result;
768
+ }
769
+ const rootKey = findRootKey(parsed);
770
+ result.actualRootKey = rootKey;
771
+ if (!rootKey) {
772
+ return result;
773
+ }
774
+ const serversObj = parsed[rootKey];
775
+ if (typeof serversObj !== "object" || serversObj === null || Array.isArray(serversObj)) {
776
+ result.parseErrors.push(`"${rootKey}" must be an object`);
777
+ return result;
778
+ }
779
+ const lines = content.split("\n");
780
+ for (const [name, value] of Object.entries(serversObj)) {
781
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
782
+ continue;
783
+ }
784
+ const raw = value;
785
+ const line = findServerLine(lines, name);
786
+ const transport = inferTransport(raw);
787
+ const entry = {
788
+ name,
789
+ transport,
790
+ line,
791
+ raw
792
+ };
793
+ if (typeof raw.command === "string") entry.command = raw.command;
794
+ if (Array.isArray(raw.args)) entry.args = raw.args.map(String);
795
+ if (isStringRecord(raw.env)) entry.env = raw.env;
796
+ if (typeof raw.url === "string") entry.url = raw.url;
797
+ if (isStringRecord(raw.headers)) entry.headers = raw.headers;
798
+ if (typeof raw.disabled === "boolean") entry.disabled = raw.disabled;
799
+ if (Array.isArray(raw.autoApprove)) entry.autoApprove = raw.autoApprove.map(String);
800
+ if (typeof raw.timeout === "number") entry.timeout = raw.timeout;
801
+ if (typeof raw.oauth === "object" && raw.oauth !== null)
802
+ entry.oauth = raw.oauth;
803
+ if (typeof raw.headersHelper === "string") entry.headersHelper = raw.headersHelper;
804
+ result.servers.push(entry);
805
+ }
806
+ return result;
807
+ }
808
+ function detectClient(relativePath) {
809
+ const normalized = relativePath.replace(/\\/g, "/");
810
+ if (normalized.includes(".vscode/")) return "vscode";
811
+ if (normalized.includes(".cursor/")) return "cursor";
812
+ if (normalized.includes(".amazonq/") || normalized.includes(".aws/amazonq/")) return "amazonq";
813
+ if (normalized.includes(".continue/")) return "continue";
814
+ if (normalized.includes(".codeium/windsurf/")) return "windsurf";
815
+ if (normalized.includes("Claude/claude_desktop_config.json") || normalized.includes("Application Support/Claude/")) {
816
+ return "claude-desktop";
817
+ }
818
+ return "claude-code";
819
+ }
820
+ function detectScope(_relativePath) {
821
+ return "project";
822
+ }
823
+ function findRootKey(parsed) {
824
+ if ("mcpServers" in parsed) return "mcpServers";
825
+ if ("servers" in parsed) return "servers";
826
+ return null;
827
+ }
828
+ function inferTransport(raw) {
829
+ if (typeof raw.type === "string") {
830
+ if (raw.type === "stdio" || raw.type === "http" || raw.type === "sse") {
831
+ return raw.type;
832
+ }
833
+ return "unknown";
834
+ }
835
+ if ("command" in raw) return "stdio";
836
+ if ("url" in raw) return "http";
837
+ return "unknown";
838
+ }
839
+ function findServerLine(lines, serverName) {
840
+ const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
841
+ const pattern = new RegExp(`"${escaped}"\\s*:`);
842
+ for (let i = 0; i < lines.length; i++) {
843
+ if (pattern.test(lines[i])) {
844
+ return i + 1;
845
+ }
846
+ }
847
+ return 1;
848
+ }
849
+ function isStringRecord(value) {
850
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
851
+ return Object.values(value).every((v) => typeof v === "string");
852
+ }
853
+ function checkGitTracked(filePath, projectRoot) {
854
+ try {
855
+ execSync(`git ls-files --error-unmatch "${filePath}"`, {
856
+ cwd: projectRoot,
857
+ stdio: "pipe"
858
+ });
859
+ return true;
860
+ } catch {
861
+ return false;
862
+ }
863
+ }
864
+
865
+ // src/core/checks/commands.ts
866
+ import * as fs3 from "fs";
867
+ import * as path4 from "path";
868
+ var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
869
+ var MAKE_PATTERN = /^make\s+(\S+)/;
870
+ var NPX_PATTERN = /^npx\s+(\S+)/;
871
+ async function checkCommands(file, projectRoot) {
872
+ const issues = [];
873
+ const pkgJson = loadPackageJson(projectRoot);
874
+ const makefile = loadMakefile(projectRoot);
875
+ for (const ref of file.references.commands) {
876
+ const cmd = ref.value;
877
+ const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
878
+ if (scriptMatch && pkgJson) {
879
+ const scriptName = scriptMatch[1];
880
+ if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
881
+ const available = Object.keys(pkgJson.scripts).join(", ");
882
+ issues.push({
883
+ severity: "error",
884
+ check: "commands",
885
+ line: ref.line,
886
+ message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`,
887
+ suggestion: available ? `Available scripts: ${available}` : void 0
888
+ });
889
+ }
890
+ continue;
891
+ }
892
+ const shorthandMatch = cmd.match(
893
+ /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/
894
+ );
895
+ if (shorthandMatch && pkgJson) {
896
+ const scriptName = shorthandMatch[2];
897
+ if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
898
+ issues.push({
899
+ severity: "error",
900
+ check: "commands",
901
+ line: ref.line,
902
+ message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`
903
+ });
904
+ }
905
+ continue;
906
+ }
907
+ const npxMatch = cmd.match(NPX_PATTERN);
908
+ if (npxMatch && pkgJson) {
909
+ const pkgName = npxMatch[1];
910
+ if (pkgName.startsWith("-")) continue;
911
+ const allDeps = {
912
+ ...pkgJson.dependencies,
913
+ ...pkgJson.devDependencies
914
+ };
915
+ if (!(pkgName in allDeps)) {
916
+ const binPath = path4.join(projectRoot, "node_modules", ".bin", pkgName);
917
+ try {
918
+ fs3.accessSync(binPath);
919
+ } catch {
920
+ issues.push({
921
+ severity: "warning",
922
+ check: "commands",
923
+ line: ref.line,
924
+ message: `"${cmd}" \u2014 "${pkgName}" not found in dependencies`,
925
+ suggestion: "If this is a global tool, consider adding it to devDependencies for reproducibility"
926
+ });
927
+ }
928
+ }
929
+ continue;
930
+ }
931
+ const makeMatch = cmd.match(MAKE_PATTERN);
932
+ if (makeMatch) {
933
+ const target = makeMatch[1];
934
+ if (makefile && !hasMakeTarget(makefile, target)) {
935
+ issues.push({
936
+ severity: "error",
937
+ check: "commands",
938
+ line: ref.line,
939
+ message: `"${cmd}" \u2014 target "${target}" not found in Makefile`
940
+ });
941
+ } else if (!makefile) {
942
+ issues.push({
943
+ severity: "error",
944
+ check: "commands",
945
+ line: ref.line,
946
+ message: `"${cmd}" \u2014 no Makefile found in project`
947
+ });
948
+ }
949
+ continue;
950
+ }
951
+ const toolMatch = cmd.match(/^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/);
952
+ if (toolMatch && pkgJson) {
953
+ const tool = toolMatch[1];
954
+ const allDeps = {
955
+ ...pkgJson.dependencies,
956
+ ...pkgJson.devDependencies
957
+ };
958
+ if (!(tool in allDeps)) {
959
+ const binPath = path4.join(projectRoot, "node_modules", ".bin", tool);
960
+ try {
961
+ fs3.accessSync(binPath);
962
+ } catch {
963
+ issues.push({
964
+ severity: "warning",
965
+ check: "commands",
966
+ line: ref.line,
967
+ message: `"${cmd}" \u2014 "${tool}" not found in dependencies or node_modules/.bin`
968
+ });
969
+ }
970
+ }
971
+ }
972
+ }
973
+ return issues;
974
+ }
975
+ function loadMakefile(projectRoot) {
976
+ try {
977
+ return fs3.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8");
978
+ } catch {
979
+ return null;
980
+ }
981
+ }
982
+ function hasMakeTarget(makefile, target) {
983
+ const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
984
+ return pattern.test(makefile);
985
+ }
986
+
987
+ // src/core/checks/staleness.ts
988
+ import * as path5 from "path";
989
+ var WARNING_DAYS = 30;
990
+ var INFO_DAYS = 14;
991
+ async function checkStaleness(file, projectRoot) {
992
+ const issues = [];
993
+ if (!await isGitRepo(projectRoot)) {
994
+ return issues;
995
+ }
996
+ const relativePath = path5.relative(projectRoot, file.filePath).replace(/\\/g, "/");
997
+ const lastModified = await getFileLastModified(projectRoot, relativePath);
998
+ if (!lastModified || isNaN(lastModified.getTime())) {
999
+ return issues;
1000
+ }
1001
+ const daysSinceUpdate = Math.floor((Date.now() - lastModified.getTime()) / (1e3 * 60 * 60 * 24));
1002
+ if (daysSinceUpdate < INFO_DAYS) {
1003
+ return issues;
1004
+ }
1005
+ const referencedPaths = /* @__PURE__ */ new Set();
1006
+ for (const ref of file.references.paths) {
1007
+ const parts = ref.value.split("/");
1008
+ if (parts.length > 1) {
1009
+ referencedPaths.add(parts.slice(0, -1).join("/"));
1010
+ }
1011
+ referencedPaths.add(ref.value);
1012
+ }
1013
+ let totalCommits = 0;
1014
+ let mostActiveRef = "";
1015
+ let mostActiveCommits = 0;
1016
+ for (const refPath of referencedPaths) {
1017
+ const commits = await getCommitsSince(projectRoot, refPath, lastModified);
1018
+ totalCommits += commits;
1019
+ if (commits > mostActiveCommits) {
1020
+ mostActiveCommits = commits;
1021
+ mostActiveRef = refPath;
1022
+ }
1023
+ }
1024
+ if (totalCommits === 0) {
1025
+ return issues;
1026
+ }
1027
+ const severity = daysSinceUpdate >= WARNING_DAYS ? "warning" : "info";
1028
+ issues.push({
1029
+ severity,
1030
+ check: "staleness",
1031
+ line: 1,
1032
+ message: `Last updated ${daysSinceUpdate} days ago. ${mostActiveRef} has ${mostActiveCommits} commits since.`,
1033
+ suggestion: "Review and update this context file to reflect recent changes.",
1034
+ detail: `${totalCommits} total commits to referenced paths since last update.`
1035
+ });
1036
+ return issues;
1037
+ }
1038
+
1039
+ // src/core/checks/redundancy.ts
1040
+ import * as path6 from "path";
1041
+ var PACKAGE_TECH_MAP = {
1042
+ react: ["React", "react"],
1043
+ "react-dom": ["React DOM", "ReactDOM"],
1044
+ next: ["Next.js", "NextJS", "next.js"],
1045
+ express: ["Express", "express.js"],
1046
+ fastify: ["Fastify"],
1047
+ typescript: ["TypeScript"],
1048
+ vue: ["Vue", "Vue.js", "vue.js"],
1049
+ angular: ["Angular"],
1050
+ svelte: ["Svelte", "SvelteKit"],
1051
+ tailwindcss: ["Tailwind", "TailwindCSS", "tailwind"],
1052
+ prisma: ["Prisma"],
1053
+ drizzle: ["Drizzle"],
1054
+ "drizzle-orm": ["Drizzle"],
1055
+ jest: ["Jest"],
1056
+ vitest: ["Vitest"],
1057
+ mocha: ["Mocha"],
1058
+ eslint: ["ESLint"],
1059
+ prettier: ["Prettier"],
1060
+ webpack: ["Webpack"],
1061
+ vite: ["Vite"],
1062
+ esbuild: ["esbuild"],
1063
+ tsup: ["tsup"],
1064
+ rollup: ["Rollup"],
1065
+ graphql: ["GraphQL"],
1066
+ mongoose: ["Mongoose"],
1067
+ sequelize: ["Sequelize"],
1068
+ "socket.io": ["Socket.IO", "socket.io"],
1069
+ redis: ["Redis"],
1070
+ ioredis: ["Redis"],
1071
+ postgres: ["PostgreSQL", "Postgres"],
1072
+ pg: ["PostgreSQL", "Postgres"],
1073
+ mysql2: ["MySQL"],
1074
+ sqlite3: ["SQLite"],
1075
+ "better-sqlite3": ["SQLite"],
1076
+ zod: ["Zod"],
1077
+ joi: ["Joi"],
1078
+ axios: ["Axios"],
1079
+ lodash: ["Lodash", "lodash"],
1080
+ underscore: ["Underscore"],
1081
+ moment: ["Moment", "moment.js"],
1082
+ dayjs: ["Day.js", "dayjs"],
1083
+ "date-fns": ["date-fns"],
1084
+ docker: ["Docker"],
1085
+ kubernetes: ["Kubernetes", "K8s"],
1086
+ terraform: ["Terraform"],
1087
+ storybook: ["Storybook"],
1088
+ playwright: ["Playwright"],
1089
+ cypress: ["Cypress"],
1090
+ puppeteer: ["Puppeteer"]
1091
+ };
1092
+ function compilePatterns(allDeps) {
1093
+ const compiled = [];
1094
+ for (const [pkg, mentions] of Object.entries(PACKAGE_TECH_MAP)) {
1095
+ if (!allDeps.has(pkg)) continue;
1096
+ for (const mention of mentions) {
1097
+ const escaped = escapeRegex(mention);
1098
+ compiled.push({
1099
+ pkg,
1100
+ mention,
1101
+ patterns: [
1102
+ new RegExp(`\\b(?:use|using|built with|powered by|written in)\\s+${escaped}\\b`, "i"),
1103
+ new RegExp(`\\bwe\\s+use\\s+${escaped}\\b`, "i"),
1104
+ new RegExp(`\\b${escaped}\\s+(?:project|app|application|codebase)\\b`, "i"),
1105
+ new RegExp(`\\bThis is a\\s+${escaped}\\b`, "i")
1106
+ ]
1107
+ });
1108
+ }
1109
+ }
1110
+ return compiled;
1111
+ }
1112
+ async function checkRedundancy(file, projectRoot) {
1113
+ const issues = [];
1114
+ const pkgJson = loadPackageJson(projectRoot);
1115
+ if (pkgJson) {
1116
+ const allDeps = /* @__PURE__ */ new Set([
1117
+ ...Object.keys(pkgJson.dependencies || {}),
1118
+ ...Object.keys(pkgJson.devDependencies || {})
1119
+ ]);
1120
+ const compiledPatterns = compilePatterns(allDeps);
1121
+ const lines2 = file.content.split("\n");
1122
+ for (let i = 0; i < lines2.length; i++) {
1123
+ const line = lines2[i];
1124
+ for (const { pkg, mention, patterns } of compiledPatterns) {
1125
+ let matched = false;
1126
+ for (const pattern of patterns) {
1127
+ if (pattern.test(line)) {
1128
+ matched = true;
1129
+ break;
1130
+ }
1131
+ }
1132
+ if (matched) {
1133
+ const wastedTokens = countTokens(line.trim());
1134
+ issues.push({
1135
+ severity: "info",
1136
+ check: "redundancy",
1137
+ line: i + 1,
1138
+ message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : "devDependencies"} \u2014 agent can infer this`,
1139
+ suggestion: `~${wastedTokens} tokens could be saved`
1140
+ });
1141
+ }
1142
+ }
1143
+ }
1144
+ }
1145
+ const lines = file.content.split("\n");
1146
+ for (let i = 0; i < lines.length; i++) {
1147
+ const line = lines[i];
1148
+ const dirMatch = line.match(
1149
+ /(?:are|go|live|found|located|stored)\s+(?:in|at|under)\s+[`"]?(\S+\/)[`"]?/i
1150
+ );
1151
+ if (dirMatch) {
1152
+ const dir = dirMatch[1].replace(/[`"]/g, "");
1153
+ const fullPath = path6.resolve(projectRoot, dir);
1154
+ if (isDirectory(fullPath)) {
1155
+ issues.push({
1156
+ severity: "info",
1157
+ check: "redundancy",
1158
+ line: i + 1,
1159
+ message: `Directory "${dir}" exists and is discoverable \u2014 agent can find this by listing files`,
1160
+ suggestion: "Only keep if there is non-obvious context about this directory"
1161
+ });
1162
+ }
1163
+ }
1164
+ }
1165
+ return issues;
1166
+ }
1167
+ function checkDuplicateContent(files) {
1168
+ const issues = [];
1169
+ for (let i = 0; i < files.length; i++) {
1170
+ for (let j = i + 1; j < files.length; j++) {
1171
+ const overlap = calculateLineOverlap(files[i].content, files[j].content);
1172
+ if (overlap > 0.6) {
1173
+ issues.push({
1174
+ severity: "warning",
1175
+ check: "redundancy",
1176
+ line: 1,
1177
+ message: `${files[i].relativePath} and ${files[j].relativePath} have ${Math.round(overlap * 100)}% content overlap`,
1178
+ suggestion: "Consider consolidating into a single context file"
1179
+ });
1180
+ }
1181
+ }
1182
+ }
1183
+ return issues;
1184
+ }
1185
+ function calculateLineOverlap(contentA, contentB) {
1186
+ const linesA = new Set(
1187
+ contentA.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
1188
+ );
1189
+ const linesB = new Set(
1190
+ contentB.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
1191
+ );
1192
+ if (linesA.size === 0 || linesB.size === 0) return 0;
1193
+ let overlap = 0;
1194
+ for (const line of linesA) {
1195
+ if (linesB.has(line)) overlap++;
1196
+ }
1197
+ return overlap / Math.min(linesA.size, linesB.size);
1198
+ }
1199
+ function escapeRegex(str) {
1200
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1201
+ }
1202
+
1203
+ // src/core/checks/contradictions.ts
1204
+ var DIRECTIVE_CATEGORIES = [
1205
+ {
1206
+ name: "testing framework",
1207
+ options: [
1208
+ {
1209
+ label: "Jest",
1210
+ patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest.*with\s+jest\b/i]
1211
+ },
1212
+ {
1213
+ label: "Vitest",
1214
+ patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest.*with\s+vitest\b/i]
1215
+ },
1216
+ {
1217
+ label: "Mocha",
1218
+ patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest.*with\s+mocha\b/i]
1219
+ },
1220
+ {
1221
+ label: "pytest",
1222
+ patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest.*with\s+pytest\b/i]
1223
+ },
1224
+ {
1225
+ label: "Playwright",
1226
+ patterns: [/\buse\s+playwright\b/i, /\bplaywright\s+for\s+(?:e2e|test)/i]
1227
+ },
1228
+ { label: "Cypress", patterns: [/\buse\s+cypress\b/i, /\bcypress\s+for\s+(?:e2e|test)/i] }
1229
+ ]
1230
+ },
1231
+ {
1232
+ name: "package manager",
1233
+ options: [
1234
+ {
1235
+ label: "npm",
1236
+ patterns: [
1237
+ /\buse\s+npm\b/i,
1238
+ /\bnpm\s+as\s+(?:the\s+)?package\s+manager/i,
1239
+ /\balways\s+use\s+npm\b/i
1240
+ ]
1241
+ },
1242
+ {
1243
+ label: "pnpm",
1244
+ patterns: [
1245
+ /\buse\s+pnpm\b/i,
1246
+ /\bpnpm\s+as\s+(?:the\s+)?package\s+manager/i,
1247
+ /\balways\s+use\s+pnpm\b/i
1248
+ ]
1249
+ },
1250
+ {
1251
+ label: "yarn",
1252
+ patterns: [
1253
+ /\buse\s+yarn\b/i,
1254
+ /\byarn\s+as\s+(?:the\s+)?package\s+manager/i,
1255
+ /\balways\s+use\s+yarn\b/i
1256
+ ]
1257
+ },
1258
+ {
1259
+ label: "bun",
1260
+ patterns: [
1261
+ /\buse\s+bun\b/i,
1262
+ /\bbun\s+as\s+(?:the\s+)?package\s+manager/i,
1263
+ /\balways\s+use\s+bun\b/i
1264
+ ]
1265
+ }
1266
+ ]
1267
+ },
1268
+ {
1269
+ name: "indentation style",
1270
+ options: [
1271
+ {
1272
+ label: "tabs",
1273
+ patterns: [/\buse\s+tabs\b/i, /\btab\s+indentation\b/i, /\bindent\s+with\s+tabs\b/i]
1274
+ },
1275
+ {
1276
+ label: "2 spaces",
1277
+ patterns: [
1278
+ /\b2[\s-]?space\s+indent/i,
1279
+ /\bindent\s+with\s+2\s+spaces/i,
1280
+ /\b2[\s-]?space\s+tabs?\b/i
1281
+ ]
1282
+ },
1283
+ {
1284
+ label: "4 spaces",
1285
+ patterns: [
1286
+ /\b4[\s-]?space\s+indent/i,
1287
+ /\bindent\s+with\s+4\s+spaces/i,
1288
+ /\b4[\s-]?space\s+tabs?\b/i
1289
+ ]
1290
+ }
1291
+ ]
1292
+ },
1293
+ {
1294
+ name: "semicolons",
1295
+ options: [
1296
+ {
1297
+ label: "semicolons",
1298
+ patterns: [
1299
+ /\buse\s+semicolons\b/i,
1300
+ /\balways\s+(?:use\s+)?semicolons\b/i,
1301
+ /\bsemicolons:\s*(?:true|yes)\b/i
1302
+ ]
1303
+ },
1304
+ {
1305
+ label: "no semicolons",
1306
+ patterns: [
1307
+ /\bno\s+semicolons\b/i,
1308
+ /\bavoid\s+semicolons\b/i,
1309
+ /\bomit\s+semicolons\b/i,
1310
+ /\bsemicolons:\s*(?:false|no)\b/i
1311
+ ]
1312
+ }
1313
+ ]
1314
+ },
1315
+ {
1316
+ name: "quote style",
1317
+ options: [
1318
+ {
1319
+ label: "single quotes",
1320
+ patterns: [
1321
+ /\bsingle\s+quotes?\b/i,
1322
+ /\buse\s+(?:single\s+)?['']single['']?\s+quotes?\b/i,
1323
+ /\bprefer\s+single\s+quotes?\b/i
1324
+ ]
1325
+ },
1326
+ {
1327
+ label: "double quotes",
1328
+ patterns: [
1329
+ /\bdouble\s+quotes?\b/i,
1330
+ /\buse\s+(?:double\s+)?[""]double[""]?\s+quotes?\b/i,
1331
+ /\bprefer\s+double\s+quotes?\b/i
1332
+ ]
1333
+ }
1334
+ ]
1335
+ },
1336
+ {
1337
+ name: "naming convention",
1338
+ options: [
1339
+ {
1340
+ label: "camelCase",
1341
+ patterns: [/\bcamelCase\b/, /\bcamel[\s-]?case\s+(?:for|naming|convention)/i]
1342
+ },
1343
+ {
1344
+ label: "snake_case",
1345
+ patterns: [/\bsnake_case\b/, /\bsnake[\s-]?case\s+(?:for|naming|convention)/i]
1346
+ },
1347
+ {
1348
+ label: "PascalCase",
1349
+ patterns: [/\bPascalCase\b/, /\bpascal[\s-]?case\s+(?:for|naming|convention)/i]
1350
+ },
1351
+ {
1352
+ label: "kebab-case",
1353
+ patterns: [/\bkebab-case\b/, /\bkebab[\s-]?case\s+(?:for|naming|convention)/i]
1354
+ }
1355
+ ]
1356
+ },
1357
+ {
1358
+ name: "CSS approach",
1359
+ options: [
1360
+ { label: "Tailwind", patterns: [/\buse\s+tailwind/i, /\btailwind\s+for\s+styl/i] },
1361
+ {
1362
+ label: "CSS Modules",
1363
+ patterns: [/\buse\s+css\s+modules\b/i, /\bcss\s+modules\s+for\s+styl/i]
1364
+ },
1365
+ {
1366
+ label: "styled-components",
1367
+ patterns: [/\buse\s+styled[\s-]?components\b/i, /\bstyled[\s-]?components\s+for\s+styl/i]
1368
+ },
1369
+ { label: "CSS-in-JS", patterns: [/\buse\s+css[\s-]?in[\s-]?js\b/i] }
1370
+ ]
1371
+ },
1372
+ {
1373
+ name: "state management",
1374
+ options: [
1375
+ { label: "Redux", patterns: [/\buse\s+redux\b/i, /\bredux\s+for\s+state/i] },
1376
+ { label: "Zustand", patterns: [/\buse\s+zustand\b/i, /\bzustand\s+for\s+state/i] },
1377
+ { label: "MobX", patterns: [/\buse\s+mobx\b/i, /\bmobx\s+for\s+state/i] },
1378
+ { label: "Jotai", patterns: [/\buse\s+jotai\b/i, /\bjotai\s+for\s+state/i] },
1379
+ { label: "Recoil", patterns: [/\buse\s+recoil\b/i, /\brecoil\s+for\s+state/i] }
1380
+ ]
1381
+ }
1382
+ ];
1383
+ function detectDirectives(file) {
1384
+ const directives = [];
1385
+ const lines = file.content.split("\n");
1386
+ for (let i = 0; i < lines.length; i++) {
1387
+ const line = lines[i];
1388
+ for (const category of DIRECTIVE_CATEGORIES) {
1389
+ for (const option of category.options) {
1390
+ for (const pattern of option.patterns) {
1391
+ if (pattern.test(line)) {
1392
+ directives.push({
1393
+ file: file.relativePath,
1394
+ category: category.name,
1395
+ label: option.label,
1396
+ line: i + 1,
1397
+ text: line.trim()
1398
+ });
1399
+ break;
1400
+ }
1401
+ }
1402
+ }
1403
+ }
1404
+ }
1405
+ return directives;
1406
+ }
1407
+ function checkContradictions(files) {
1408
+ if (files.length < 2) return [];
1409
+ const issues = [];
1410
+ const allDirectives = [];
1411
+ for (const file of files) {
1412
+ allDirectives.push(...detectDirectives(file));
1413
+ }
1414
+ const byCategory = /* @__PURE__ */ new Map();
1415
+ for (const d of allDirectives) {
1416
+ const existing = byCategory.get(d.category) || [];
1417
+ existing.push(d);
1418
+ byCategory.set(d.category, existing);
1419
+ }
1420
+ for (const [category, directives] of byCategory) {
1421
+ const byFile = /* @__PURE__ */ new Map();
1422
+ for (const d of directives) {
1423
+ const existing = byFile.get(d.file) || [];
1424
+ existing.push(d);
1425
+ byFile.set(d.file, existing);
1426
+ }
1427
+ const labels = new Set(directives.map((d) => d.label));
1428
+ if (labels.size <= 1) continue;
1429
+ const fileLabels = /* @__PURE__ */ new Map();
1430
+ for (const d of directives) {
1431
+ const existing = fileLabels.get(d.file) || /* @__PURE__ */ new Set();
1432
+ existing.add(d.label);
1433
+ fileLabels.set(d.file, existing);
1434
+ }
1435
+ const fileEntries = [...fileLabels.entries()];
1436
+ for (let i = 0; i < fileEntries.length; i++) {
1437
+ for (let j = i + 1; j < fileEntries.length; j++) {
1438
+ const [fileA, labelsA] = fileEntries[i];
1439
+ const [fileB, labelsB] = fileEntries[j];
1440
+ for (const labelA of labelsA) {
1441
+ for (const labelB of labelsB) {
1442
+ if (labelA !== labelB) {
1443
+ const directiveA = directives.find((d) => d.file === fileA && d.label === labelA);
1444
+ const directiveB = directives.find((d) => d.file === fileB && d.label === labelB);
1445
+ issues.push({
1446
+ severity: "warning",
1447
+ check: "contradictions",
1448
+ line: directiveA.line,
1449
+ message: `${category} conflict: "${directiveA.label}" in ${fileA} vs "${directiveB.label}" in ${fileB}`,
1450
+ suggestion: `Align on one ${category} across all context files`,
1451
+ detail: `${fileA}:${directiveA.line} says "${directiveA.text}" but ${fileB}:${directiveB.line} says "${directiveB.text}"`
1452
+ });
1453
+ }
1454
+ }
1455
+ }
1456
+ }
1457
+ }
1458
+ }
1459
+ return issues;
1460
+ }
1461
+
1462
+ // src/core/checks/frontmatter.ts
1463
+ function parseFrontmatter(content) {
1464
+ const lines = content.split("\n");
1465
+ if (lines[0]?.trim() !== "---") {
1466
+ return { found: false, fields: {}, endLine: 0 };
1467
+ }
1468
+ const fields = {};
1469
+ let endLine = 0;
1470
+ for (let i = 1; i < lines.length; i++) {
1471
+ const line = lines[i].trim();
1472
+ if (line === "---") {
1473
+ endLine = i + 1;
1474
+ break;
1475
+ }
1476
+ const match = line.match(/^(\w+)\s*:\s*(.*)$/);
1477
+ if (match) {
1478
+ fields[match[1]] = match[2].trim();
1479
+ }
1480
+ }
1481
+ if (endLine === 0) {
1482
+ return { found: true, fields, endLine: lines.length };
1483
+ }
1484
+ return { found: true, fields, endLine };
1485
+ }
1486
+ function isCursorMdc(file) {
1487
+ return file.relativePath.endsWith(".mdc");
1488
+ }
1489
+ function isCopilotInstructions(file) {
1490
+ return file.relativePath.includes(".github/instructions/") && file.relativePath.endsWith(".md");
1491
+ }
1492
+ function isWindsurfRule(file) {
1493
+ return file.relativePath.includes(".windsurf/rules/") && file.relativePath.endsWith(".md");
1494
+ }
1495
+ var VALID_WINDSURF_TRIGGERS = ["always_on", "glob", "manual", "model"];
1496
+ async function checkFrontmatter(file, _projectRoot) {
1497
+ const issues = [];
1498
+ if (isCursorMdc(file)) {
1499
+ issues.push(...validateCursorMdc(file));
1500
+ } else if (isCopilotInstructions(file)) {
1501
+ issues.push(...validateCopilotInstructions(file));
1502
+ } else if (isWindsurfRule(file)) {
1503
+ issues.push(...validateWindsurfRule(file));
1504
+ }
1505
+ return issues;
1506
+ }
1507
+ function validateCursorMdc(file) {
1508
+ const issues = [];
1509
+ const fm = parseFrontmatter(file.content);
1510
+ if (!fm.found) {
1511
+ issues.push({
1512
+ severity: "warning",
1513
+ check: "frontmatter",
1514
+ line: 1,
1515
+ message: "Cursor .mdc file is missing frontmatter",
1516
+ suggestion: "Add YAML frontmatter with description, globs, and alwaysApply fields"
1517
+ });
1518
+ return issues;
1519
+ }
1520
+ if (!fm.fields["description"]) {
1521
+ issues.push({
1522
+ severity: "warning",
1523
+ check: "frontmatter",
1524
+ line: 1,
1525
+ message: 'Missing "description" field in Cursor .mdc frontmatter',
1526
+ suggestion: "Add a description so Cursor knows when to apply this rule"
1527
+ });
1528
+ }
1529
+ if (!("alwaysApply" in fm.fields) && !("globs" in fm.fields)) {
1530
+ issues.push({
1531
+ severity: "info",
1532
+ check: "frontmatter",
1533
+ line: 1,
1534
+ message: 'No "alwaysApply" or "globs" field \u2014 rule may not be applied automatically',
1535
+ suggestion: "Set alwaysApply: true or specify globs for targeted activation"
1536
+ });
1537
+ }
1538
+ if ("alwaysApply" in fm.fields) {
1539
+ const val = fm.fields["alwaysApply"].toLowerCase();
1540
+ if (!["true", "false"].includes(val)) {
1541
+ issues.push({
1542
+ severity: "error",
1543
+ check: "frontmatter",
1544
+ line: 1,
1545
+ message: `Invalid alwaysApply value: "${fm.fields["alwaysApply"]}"`,
1546
+ suggestion: "alwaysApply must be true or false"
1547
+ });
1548
+ }
1549
+ }
1550
+ if ("globs" in fm.fields) {
1551
+ const val = fm.fields["globs"];
1552
+ if (val && !val.startsWith("[") && !val.startsWith('"') && !val.includes("*") && !val.includes("/")) {
1553
+ issues.push({
1554
+ severity: "warning",
1555
+ check: "frontmatter",
1556
+ line: 1,
1557
+ message: `Possibly invalid globs value: "${val}"`,
1558
+ suggestion: 'globs should be a glob pattern like "src/**/*.ts" or an array like ["*.ts", "*.tsx"]'
1559
+ });
1560
+ }
1561
+ }
1562
+ return issues;
1563
+ }
1564
+ function validateCopilotInstructions(file) {
1565
+ const issues = [];
1566
+ const fm = parseFrontmatter(file.content);
1567
+ if (!fm.found) {
1568
+ issues.push({
1569
+ severity: "info",
1570
+ check: "frontmatter",
1571
+ line: 1,
1572
+ message: "Copilot instructions file has no frontmatter",
1573
+ suggestion: "Add applyTo frontmatter to target specific file patterns"
1574
+ });
1575
+ return issues;
1576
+ }
1577
+ if (!fm.fields["applyTo"]) {
1578
+ issues.push({
1579
+ severity: "warning",
1580
+ check: "frontmatter",
1581
+ line: 1,
1582
+ message: 'Missing "applyTo" field in Copilot instructions frontmatter',
1583
+ suggestion: 'Add applyTo to specify which files this instruction applies to (e.g., applyTo: "**/*.ts")'
1584
+ });
1585
+ }
1586
+ return issues;
1587
+ }
1588
+ function validateWindsurfRule(file) {
1589
+ const issues = [];
1590
+ const fm = parseFrontmatter(file.content);
1591
+ if (!fm.found) {
1592
+ issues.push({
1593
+ severity: "info",
1594
+ check: "frontmatter",
1595
+ line: 1,
1596
+ message: "Windsurf rule file has no frontmatter",
1597
+ suggestion: "Add YAML frontmatter with a trigger field (always_on, glob, manual, model)"
1598
+ });
1599
+ return issues;
1600
+ }
1601
+ if (!fm.fields["trigger"]) {
1602
+ issues.push({
1603
+ severity: "warning",
1604
+ check: "frontmatter",
1605
+ line: 1,
1606
+ message: 'Missing "trigger" field in Windsurf rule frontmatter',
1607
+ suggestion: `Set trigger to one of: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
1608
+ });
1609
+ } else {
1610
+ const trigger = fm.fields["trigger"].replace(/['"]/g, "");
1611
+ if (!VALID_WINDSURF_TRIGGERS.includes(trigger)) {
1612
+ issues.push({
1613
+ severity: "error",
1614
+ check: "frontmatter",
1615
+ line: 1,
1616
+ message: `Invalid trigger value: "${trigger}"`,
1617
+ suggestion: `Valid triggers: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
1618
+ });
1619
+ }
1620
+ }
1621
+ return issues;
1622
+ }
1623
+
1624
+ // src/core/checks/mcp/schema.ts
1625
+ async function checkMcpSchema(config, _projectRoot) {
1626
+ const issues = [];
1627
+ if (config.parseErrors.length > 0) {
1628
+ for (const err of config.parseErrors) {
1629
+ issues.push({
1630
+ severity: "error",
1631
+ check: "mcp-schema",
1632
+ ruleId: "invalid-json",
1633
+ line: 1,
1634
+ message: `MCP config is not valid JSON: ${err}`
1635
+ });
1636
+ }
1637
+ return issues;
1638
+ }
1639
+ if (!config.actualRootKey) {
1640
+ issues.push({
1641
+ severity: "error",
1642
+ check: "mcp-schema",
1643
+ ruleId: "missing-root-key",
1644
+ line: 1,
1645
+ message: `MCP config has no "${config.expectedRootKey}" key`
1646
+ });
1647
+ return issues;
1648
+ }
1649
+ if (config.actualRootKey !== config.expectedRootKey) {
1650
+ const line = findKeyLine(config.content, config.actualRootKey);
1651
+ issues.push({
1652
+ severity: "error",
1653
+ check: "mcp-schema",
1654
+ ruleId: "wrong-root-key",
1655
+ line,
1656
+ message: `${config.relativePath} must use "${config.expectedRootKey}" as root key, not "${config.actualRootKey}"`,
1657
+ fix: {
1658
+ file: config.filePath,
1659
+ line,
1660
+ oldText: `"${config.actualRootKey}"`,
1661
+ newText: `"${config.expectedRootKey}"`
1662
+ }
1663
+ });
1664
+ }
1665
+ if (config.servers.length === 0 && config.actualRootKey) {
1666
+ issues.push({
1667
+ severity: "info",
1668
+ check: "mcp-schema",
1669
+ ruleId: "empty-servers",
1670
+ line: 1,
1671
+ message: "MCP config has no server entries"
1672
+ });
1673
+ return issues;
1674
+ }
1675
+ for (const server of config.servers) {
1676
+ if (!server.name) {
1677
+ issues.push({
1678
+ severity: "error",
1679
+ check: "mcp-schema",
1680
+ ruleId: "no-name-field",
1681
+ line: server.line,
1682
+ message: "Server name cannot be empty"
1683
+ });
1684
+ continue;
1685
+ }
1686
+ if (server.transport === "unknown") {
1687
+ const typeVal = server.raw.type;
1688
+ if (typeof typeVal === "string") {
1689
+ issues.push({
1690
+ severity: "warning",
1691
+ check: "mcp-schema",
1692
+ ruleId: "unknown-transport",
1693
+ line: server.line,
1694
+ message: `Server "${server.name}" has unknown transport type "${typeVal}"`
1695
+ });
1696
+ }
1697
+ }
1698
+ if (server.command && server.url) {
1699
+ issues.push({
1700
+ severity: "warning",
1701
+ check: "mcp-schema",
1702
+ ruleId: "ambiguous-transport",
1703
+ line: server.line,
1704
+ message: `Server "${server.name}" has both "command" and "url" \u2014 transport is ambiguous`
1705
+ });
1706
+ }
1707
+ if (server.transport === "stdio" && !server.command) {
1708
+ issues.push({
1709
+ severity: "error",
1710
+ check: "mcp-schema",
1711
+ ruleId: "missing-command",
1712
+ line: server.line,
1713
+ message: `Server "${server.name}" has no "command" field`
1714
+ });
1715
+ }
1716
+ if ((server.transport === "http" || server.transport === "sse") && !server.url) {
1717
+ issues.push({
1718
+ severity: "error",
1719
+ check: "mcp-schema",
1720
+ ruleId: "missing-url",
1721
+ line: server.line,
1722
+ message: `Server "${server.name}" has no "url" field`
1723
+ });
1724
+ }
1725
+ }
1726
+ return issues;
1727
+ }
1728
+ function findKeyLine(content, key) {
1729
+ const lines = content.split("\n");
1730
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1731
+ const pattern = new RegExp(`"${escaped}"\\s*:`);
1732
+ for (let i = 0; i < lines.length; i++) {
1733
+ if (pattern.test(lines[i])) {
1734
+ return i + 1;
1735
+ }
1736
+ }
1737
+ return 1;
1738
+ }
1739
+
1740
+ // src/core/checks/mcp/security.ts
1741
+ var API_KEY_PATTERNS = [
1742
+ /sk-[a-zA-Z0-9]{20,}/,
1743
+ // OpenAI / generic
1744
+ /ghp_[a-zA-Z0-9]{36}/,
1745
+ // GitHub PAT
1746
+ /ghu_[a-zA-Z0-9]{36}/,
1747
+ // GitHub user token
1748
+ /github_pat_[a-zA-Z0-9_]{80,}/,
1749
+ // GitHub fine-grained PAT
1750
+ /xoxb-[0-9]{10,}/,
1751
+ // Slack bot
1752
+ /xoxp-[0-9]{10,}/,
1753
+ // Slack user
1754
+ /AKIA[0-9A-Z]{16}/,
1755
+ // AWS access key
1756
+ /AGE-SECRET-KEY-1[a-zA-Z0-9]+/,
1757
+ // age encryption key
1758
+ /glpat-[a-zA-Z0-9_\-]{20}/,
1759
+ // GitLab PAT
1760
+ /sq0atp-[a-zA-Z0-9_\-]{22}/
1761
+ // Square
1762
+ ];
1763
+ var ENV_VAR_REF = /\$\{[^}]+\}/;
1764
+ var HIGH_ENTROPY_PATTERN = /^[A-Za-z0-9+/=_-]{21,}$/;
1765
+ var URL_SECRET_PARAMS = /[?&](key|token|api_key|apikey|secret|password|access_token)=/i;
1766
+ function isEnvVarRef(value) {
1767
+ return ENV_VAR_REF.test(value);
1768
+ }
1769
+ function isKnownApiKey(value) {
1770
+ return API_KEY_PATTERNS.some((p) => p.test(value));
1771
+ }
1772
+ function isHighEntropySecret(value) {
1773
+ if (isEnvVarRef(value)) return false;
1774
+ return HIGH_ENTROPY_PATTERN.test(value);
1775
+ }
1776
+ function deriveEnvVarName(serverName, suffix) {
1777
+ return serverName.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase().replace(/^_|_$/g, "") + "_" + suffix;
1778
+ }
1779
+ async function checkMcpSecurity(config, _projectRoot) {
1780
+ const issues = [];
1781
+ if (!config.isGitTracked) return issues;
1782
+ if (config.parseErrors.length > 0) return issues;
1783
+ for (const server of config.servers) {
1784
+ if (server.headers) {
1785
+ for (const [headerName, headerValue] of Object.entries(server.headers)) {
1786
+ if (headerName.toLowerCase() === "authorization") {
1787
+ const bearerMatch = headerValue.match(/^Bearer\s+(.+)$/i);
1788
+ if (bearerMatch) {
1789
+ const token = bearerMatch[1];
1790
+ if (!isEnvVarRef(token)) {
1791
+ const envVar = deriveEnvVarName(server.name, "API_KEY");
1792
+ issues.push({
1793
+ severity: "error",
1794
+ check: "mcp-security",
1795
+ ruleId: "hardcoded-bearer",
1796
+ line: server.line,
1797
+ message: `Server "${server.name}" has a hardcoded Bearer token in a git-tracked file`,
1798
+ fix: {
1799
+ file: config.filePath,
1800
+ line: server.line,
1801
+ oldText: `Bearer ${token}`,
1802
+ newText: `Bearer \${${envVar}}`
1803
+ }
1804
+ });
1805
+ }
1806
+ }
1807
+ }
1808
+ if (!isEnvVarRef(headerValue) && isKnownApiKey(headerValue)) {
1809
+ issues.push({
1810
+ severity: "error",
1811
+ check: "mcp-security",
1812
+ ruleId: "hardcoded-api-key",
1813
+ line: server.line,
1814
+ message: `Server "${server.name}" has a hardcoded API key in a git-tracked file`
1815
+ });
1816
+ }
1817
+ }
1818
+ }
1819
+ if (server.env) {
1820
+ for (const envValue of Object.values(server.env)) {
1821
+ if (!isEnvVarRef(envValue) && (isKnownApiKey(envValue) || isHighEntropySecret(envValue))) {
1822
+ const envVar = deriveEnvVarName(server.name, "API_KEY");
1823
+ issues.push({
1824
+ severity: "error",
1825
+ check: "mcp-security",
1826
+ ruleId: "hardcoded-api-key",
1827
+ line: server.line,
1828
+ message: `Server "${server.name}" has a hardcoded API key in a git-tracked file`,
1829
+ fix: {
1830
+ file: config.filePath,
1831
+ line: server.line,
1832
+ oldText: envValue,
1833
+ newText: `\${${envVar}}`
1834
+ }
1835
+ });
1836
+ }
1837
+ }
1838
+ }
1839
+ if (server.url && !isEnvVarRef(server.url) && URL_SECRET_PARAMS.test(server.url)) {
1840
+ issues.push({
1841
+ severity: "error",
1842
+ check: "mcp-security",
1843
+ ruleId: "secret-in-url",
1844
+ line: server.line,
1845
+ message: `Server "${server.name}" has a secret in the URL query string`
1846
+ });
1847
+ }
1848
+ if (server.url) {
1849
+ try {
1850
+ if (!isEnvVarRef(server.url)) {
1851
+ const parsed = new URL(server.url);
1852
+ if (parsed.protocol === "http:" && parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1" && parsed.hostname !== "::1") {
1853
+ issues.push({
1854
+ severity: "warning",
1855
+ check: "mcp-security",
1856
+ ruleId: "http-no-tls",
1857
+ line: server.line,
1858
+ message: `Server "${server.name}" uses HTTP without TLS`
1859
+ });
1860
+ }
1861
+ }
1862
+ } catch {
1863
+ }
1864
+ }
1865
+ }
1866
+ return issues;
1867
+ }
1868
+
1869
+ // src/core/checks/mcp/commands.ts
1870
+ import * as fs4 from "fs";
1871
+ import * as path7 from "path";
1872
+ var LOCAL_PATH_PATTERN = /^\.\.?\//;
1873
+ var FILE_PATH_PATTERN = /^[^-].*\/.*\.\w+$/;
1874
+ async function checkMcpCommands(config, projectRoot) {
1875
+ const issues = [];
1876
+ if (config.parseErrors.length > 0) return issues;
1877
+ for (const server of config.servers) {
1878
+ if (server.transport !== "stdio" || !server.command) continue;
1879
+ if (process.platform === "win32" && config.scope === "project" && server.command === "npx") {
1880
+ issues.push({
1881
+ severity: "error",
1882
+ check: "mcp-commands",
1883
+ ruleId: "windows-npx-no-wrapper",
1884
+ line: server.line,
1885
+ message: `Server "${server.name}": npx requires "cmd /c" wrapper on Windows`,
1886
+ suggestion: 'Change command to "cmd" and prepend "/c", "npx" to args: ["/c", "npx", ...]',
1887
+ fix: buildNpxFix(config, server.name, server.args)
1888
+ });
1889
+ }
1890
+ if (LOCAL_PATH_PATTERN.test(server.command)) {
1891
+ const resolved = path7.resolve(projectRoot, server.command);
1892
+ if (!fileExistsSafe(resolved)) {
1893
+ issues.push({
1894
+ severity: "warning",
1895
+ check: "mcp-commands",
1896
+ ruleId: "command-not-found",
1897
+ line: server.line,
1898
+ message: `Server "${server.name}": command "${server.command}" not found`
1899
+ });
1900
+ }
1901
+ }
1902
+ if (server.args) {
1903
+ for (const arg of server.args) {
1904
+ if (LOCAL_PATH_PATTERN.test(arg) || FILE_PATH_PATTERN.test(arg)) {
1905
+ const resolved = path7.resolve(projectRoot, arg);
1906
+ if (!fileExistsSafe(resolved)) {
1907
+ issues.push({
1908
+ severity: "warning",
1909
+ check: "mcp-commands",
1910
+ ruleId: "args-path-missing",
1911
+ line: server.line,
1912
+ message: `Server "${server.name}": arg "${arg}" looks like a file path but doesn't exist`
1913
+ });
1914
+ }
1915
+ }
1916
+ }
1917
+ }
1918
+ }
1919
+ return issues;
1920
+ }
1921
+ function fileExistsSafe(filePath) {
1922
+ try {
1923
+ fs4.accessSync(filePath);
1924
+ return true;
1925
+ } catch {
1926
+ return false;
1927
+ }
1928
+ }
1929
+ function buildNpxFix(config, serverName, _args) {
1930
+ const lines = config.content.split("\n");
1931
+ for (let i = 0; i < lines.length; i++) {
1932
+ const line = lines[i];
1933
+ if (line.includes('"command"') && line.includes('"npx"')) {
1934
+ let inRightServer = false;
1935
+ for (let j = i - 1; j >= 0; j--) {
1936
+ if (lines[j].includes(`"${serverName}"`)) {
1937
+ inRightServer = true;
1938
+ break;
1939
+ }
1940
+ if (lines[j].includes('"command"')) break;
1941
+ }
1942
+ if (!inRightServer) continue;
1943
+ return {
1944
+ file: config.filePath,
1945
+ line: i + 1,
1946
+ oldText: '"npx"',
1947
+ newText: '"cmd"'
1948
+ };
1949
+ }
1950
+ }
1951
+ return void 0;
1952
+ }
1953
+
1954
+ // src/core/checks/mcp/deprecated.ts
1955
+ async function checkMcpDeprecated(config, _projectRoot) {
1956
+ const issues = [];
1957
+ if (config.parseErrors.length > 0) return issues;
1958
+ for (const server of config.servers) {
1959
+ if (server.transport === "sse") {
1960
+ const line = findTypeLine(config.content, server.name) || server.line;
1961
+ issues.push({
1962
+ severity: "warning",
1963
+ check: "mcp-deprecated",
1964
+ ruleId: "sse-transport",
1965
+ line,
1966
+ message: `Server "${server.name}" uses deprecated SSE transport \u2014 use "http" (Streamable HTTP) instead`,
1967
+ fix: {
1968
+ file: config.filePath,
1969
+ line,
1970
+ oldText: '"sse"',
1971
+ newText: '"http"'
1972
+ }
1973
+ });
1974
+ }
1975
+ }
1976
+ return issues;
1977
+ }
1978
+ function findTypeLine(content, serverName) {
1979
+ const lines = content.split("\n");
1980
+ let inServer = false;
1981
+ for (let i = 0; i < lines.length; i++) {
1982
+ if (lines[i].includes(`"${serverName}"`)) {
1983
+ inServer = true;
1984
+ }
1985
+ if (inServer && lines[i].includes('"type"') && lines[i].includes('"sse"')) {
1986
+ return i + 1;
1987
+ }
1988
+ if (inServer && i > 0 && lines[i].match(/^\s{4}"\w/) && !lines[i].includes(`"${serverName}"`)) {
1989
+ const indent = lines[i].search(/\S/);
1990
+ const serverIndent = lines.findIndex((l) => l.includes(`"${serverName}"`));
1991
+ if (serverIndent >= 0) {
1992
+ const origIndent = lines[serverIndent].search(/\S/);
1993
+ if (indent <= origIndent) break;
1994
+ }
1995
+ }
1996
+ }
1997
+ return null;
1998
+ }
1999
+
2000
+ // src/core/checks/mcp/env.ts
2001
+ var HAS_CURSOR_SYNTAX = /\$\{env:[^}]+\}/;
2002
+ var HAS_CLAUDE_SYNTAX = /\$\{[A-Za-z_][A-Za-z0-9_]*(?::-.*)?\}/;
2003
+ var HAS_CONTINUE_SYNTAX = /\$\{\{\s*secrets\.[^}]+\}\}/;
2004
+ var ANY_ENV_REF = /\$\{[^}]+\}/g;
2005
+ function extractEnvVarRefs(value) {
2006
+ const refs = [];
2007
+ let match;
2008
+ ANY_ENV_REF.lastIndex = 0;
2009
+ while ((match = ANY_ENV_REF.exec(value)) !== null) {
2010
+ const full = match[0];
2011
+ let varName = null;
2012
+ const cursorMatch = full.match(/^\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}$/);
2013
+ if (cursorMatch) {
2014
+ varName = cursorMatch[1];
2015
+ }
2016
+ const continueMatch = full.match(/^\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}$/);
2017
+ if (continueMatch) {
2018
+ varName = continueMatch[1];
2019
+ }
2020
+ const claudeMatch = full.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-.*)?\}$/);
2021
+ if (!varName && claudeMatch) {
2022
+ varName = claudeMatch[1];
2023
+ }
2024
+ if (varName) {
2025
+ refs.push({ varName, fullMatch: full });
2026
+ }
2027
+ }
2028
+ return refs;
2029
+ }
2030
+ function collectAllStringValues(server) {
2031
+ const values = [];
2032
+ if (server.command) values.push(server.command);
2033
+ if (server.args) values.push(...server.args);
2034
+ if (server.url) values.push(server.url);
2035
+ if (server.headers) values.push(...Object.values(server.headers));
2036
+ if (server.env) values.push(...Object.values(server.env));
2037
+ return values;
2038
+ }
2039
+ async function checkMcpEnv(config, _projectRoot) {
2040
+ const issues = [];
2041
+ if (config.parseErrors.length > 0) return issues;
2042
+ for (const server of config.servers) {
2043
+ const allValues = collectAllStringValues(server);
2044
+ for (const value of allValues) {
2045
+ if (config.client === "claude-code") {
2046
+ if (HAS_CURSOR_SYNTAX.test(value)) {
2047
+ issues.push({
2048
+ severity: "error",
2049
+ check: "mcp-env",
2050
+ ruleId: "wrong-syntax",
2051
+ line: server.line,
2052
+ message: `Server "${server.name}": Claude Code uses \${VAR}, not \${env:VAR}`,
2053
+ fix: buildSyntaxFix(config, server.line, value, "claude-code")
2054
+ });
2055
+ }
2056
+ }
2057
+ if (config.client === "cursor") {
2058
+ if (HAS_CLAUDE_SYNTAX.test(value) && !HAS_CURSOR_SYNTAX.test(value) && !HAS_CONTINUE_SYNTAX.test(value)) {
2059
+ issues.push({
2060
+ severity: "error",
2061
+ check: "mcp-env",
2062
+ ruleId: "wrong-syntax",
2063
+ line: server.line,
2064
+ message: `Server "${server.name}": Cursor uses \${env:VAR}, not \${VAR}`,
2065
+ fix: buildSyntaxFix(config, server.line, value, "cursor")
2066
+ });
2067
+ }
2068
+ }
2069
+ if (config.client === "continue") {
2070
+ if ((HAS_CLAUDE_SYNTAX.test(value) || HAS_CURSOR_SYNTAX.test(value)) && !HAS_CONTINUE_SYNTAX.test(value)) {
2071
+ issues.push({
2072
+ severity: "error",
2073
+ check: "mcp-env",
2074
+ ruleId: "wrong-syntax",
2075
+ line: server.line,
2076
+ message: `Server "${server.name}": Continue uses \${{ secrets.VAR }}, not \${VAR}`,
2077
+ fix: buildSyntaxFix(config, server.line, value, "continue")
2078
+ });
2079
+ }
2080
+ }
2081
+ }
2082
+ for (const value of allValues) {
2083
+ const refs = extractEnvVarRefs(value);
2084
+ for (const ref of refs) {
2085
+ if (!(ref.varName in process.env)) {
2086
+ issues.push({
2087
+ severity: "info",
2088
+ check: "mcp-env",
2089
+ ruleId: "unset-variable",
2090
+ line: server.line,
2091
+ message: `Server "${server.name}": environment variable "${ref.varName}" is not set`
2092
+ });
2093
+ }
2094
+ }
2095
+ }
2096
+ if (server.env && Object.keys(server.env).length === 0) {
2097
+ issues.push({
2098
+ severity: "info",
2099
+ check: "mcp-env",
2100
+ ruleId: "empty-env-block",
2101
+ line: server.line,
2102
+ message: `Server "${server.name}": empty "env" block can be removed`
2103
+ });
2104
+ }
2105
+ }
2106
+ return issues;
2107
+ }
2108
+ function buildSyntaxFix(config, line, value, targetClient) {
2109
+ if (targetClient === "claude-code") {
2110
+ const fixed = value.replace(/\$\{env:([^}]+)\}/g, "${$1}");
2111
+ return { file: config.filePath, line, oldText: value, newText: fixed };
2112
+ }
2113
+ if (targetClient === "cursor") {
2114
+ const fixed = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-.*)?\}/g, (match, varName) => {
2115
+ if (match.startsWith("${env:")) return match;
2116
+ return `\${env:${varName}}`;
2117
+ });
2118
+ return { file: config.filePath, line, oldText: value, newText: fixed };
2119
+ }
2120
+ if (targetClient === "continue") {
2121
+ let fixed = value.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "${{ secrets.$1 }}");
2122
+ fixed = fixed.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-.*)?\}/g, (match) => {
2123
+ if (match.includes("secrets.")) return match;
2124
+ const varMatch = match.match(/\$\{([A-Za-z_][A-Za-z0-9_]*)/);
2125
+ return varMatch ? `\${{ secrets.${varMatch[1]} }}` : match;
2126
+ });
2127
+ return { file: config.filePath, line, oldText: value, newText: fixed };
2128
+ }
2129
+ return void 0;
2130
+ }
2131
+
2132
+ // src/core/checks/mcp/urls.ts
2133
+ var ENV_VAR_REF2 = /\$\{[^}]+\}/;
2134
+ async function checkMcpUrls(config, _projectRoot) {
2135
+ const issues = [];
2136
+ if (config.parseErrors.length > 0) return issues;
2137
+ for (const server of config.servers) {
2138
+ if (!server.url) continue;
2139
+ if (ENV_VAR_REF2.test(server.url)) continue;
2140
+ let parsed;
2141
+ try {
2142
+ parsed = new URL(server.url);
2143
+ } catch {
2144
+ issues.push({
2145
+ severity: "error",
2146
+ check: "mcp-urls",
2147
+ ruleId: "malformed-url",
2148
+ line: server.line,
2149
+ message: `Server "${server.name}": invalid URL "${server.url}"`
2150
+ });
2151
+ continue;
2152
+ }
2153
+ if (config.scope === "project" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1")) {
2154
+ issues.push({
2155
+ severity: "warning",
2156
+ check: "mcp-urls",
2157
+ ruleId: "localhost-in-project-config",
2158
+ line: server.line,
2159
+ message: `Server "${server.name}": localhost URL in project config won't work for teammates`
2160
+ });
2161
+ }
2162
+ if (!parsed.pathname || parsed.pathname === "/") {
2163
+ issues.push({
2164
+ severity: "info",
2165
+ check: "mcp-urls",
2166
+ ruleId: "missing-path",
2167
+ line: server.line,
2168
+ message: `Server "${server.name}": URL has no path \u2014 most MCP servers expect /mcp`
2169
+ });
2170
+ }
2171
+ }
2172
+ return issues;
2173
+ }
2174
+
2175
+ // src/core/checks/mcp/consistency.ts
2176
+ async function checkMcpConsistency(configs) {
2177
+ const issues = [];
2178
+ if (configs.length < 2) {
2179
+ return checkSingleFileIssues(configs).concat(checkMissingFromClient(configs));
2180
+ }
2181
+ const serverMap = /* @__PURE__ */ new Map();
2182
+ for (const config of configs) {
2183
+ for (const server of config.servers) {
2184
+ const existing = serverMap.get(server.name) || [];
2185
+ existing.push({
2186
+ config,
2187
+ command: server.command,
2188
+ url: server.url,
2189
+ args: server.args,
2190
+ line: server.line
2191
+ });
2192
+ serverMap.set(server.name, existing);
2193
+ }
2194
+ }
2195
+ for (const [name, entries] of serverMap) {
2196
+ if (entries.length < 2) continue;
2197
+ for (let i = 0; i < entries.length; i++) {
2198
+ for (let j = i + 1; j < entries.length; j++) {
2199
+ const a = entries[i];
2200
+ const b = entries[j];
2201
+ const aKey = JSON.stringify({ cmd: a.command, url: a.url, args: a.args });
2202
+ const bKey = JSON.stringify({ cmd: b.command, url: b.url, args: b.args });
2203
+ if (aKey !== bKey) {
2204
+ issues.push({
2205
+ severity: "warning",
2206
+ check: "mcp-consistency",
2207
+ ruleId: "same-server-different-config",
2208
+ line: a.line,
2209
+ message: `Server "${name}" is configured differently in ${a.config.relativePath} and ${b.config.relativePath}`
2210
+ });
2211
+ }
2212
+ }
2213
+ }
2214
+ }
2215
+ issues.push(...checkMissingFromClient(configs));
2216
+ issues.push(...checkSingleFileIssues(configs));
2217
+ return issues;
2218
+ }
2219
+ function checkMissingFromClient(configs) {
2220
+ const issues = [];
2221
+ const primary = configs.find((c) => c.relativePath === ".mcp.json" && c.scope === "project");
2222
+ if (!primary) return issues;
2223
+ const otherConfigs = configs.filter(
2224
+ (c) => c !== primary && c.scope === "project" && (c.relativePath.includes(".cursor/") || c.relativePath.includes(".vscode/") || c.relativePath.includes(".amazonq/"))
2225
+ );
2226
+ for (const other of otherConfigs) {
2227
+ const otherNames = new Set(other.servers.map((s) => s.name));
2228
+ for (const primaryServer of primary.servers) {
2229
+ if (!otherNames.has(primaryServer.name)) {
2230
+ issues.push({
2231
+ severity: "info",
2232
+ check: "mcp-consistency",
2233
+ ruleId: "missing-from-client",
2234
+ line: primaryServer.line,
2235
+ message: `Server "${primaryServer.name}" is in .mcp.json but missing from ${other.relativePath}`
2236
+ });
2237
+ }
2238
+ }
2239
+ }
2240
+ return issues;
2241
+ }
2242
+ function checkSingleFileIssues(configs) {
2243
+ const issues = [];
2244
+ for (const config of configs) {
2245
+ const nameCount = /* @__PURE__ */ new Map();
2246
+ const lines = config.content.split("\n");
2247
+ for (const server of config.servers) {
2248
+ const escaped = server.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2249
+ const pattern = new RegExp(`"${escaped}"\\s*:`, "g");
2250
+ let count = 0;
2251
+ for (const line of lines) {
2252
+ if (pattern.test(line)) count++;
2253
+ pattern.lastIndex = 0;
2254
+ }
2255
+ nameCount.set(server.name, count);
2256
+ }
2257
+ for (const [name, count] of nameCount) {
2258
+ if (count > 1) {
2259
+ issues.push({
2260
+ severity: "warning",
2261
+ check: "mcp-consistency",
2262
+ ruleId: "duplicate-server-name",
2263
+ line: 1,
2264
+ message: `Duplicate server name "${name}" in ${config.relativePath} \u2014 only the last definition is used`
2265
+ });
2266
+ }
2267
+ }
2268
+ }
2269
+ return issues;
2270
+ }
2271
+
2272
+ // src/core/checks/mcp/redundancy.ts
2273
+ async function checkMcpRedundancy(configs) {
2274
+ const issues = [];
2275
+ for (const config of configs) {
2276
+ for (const server of config.servers) {
2277
+ if (server.disabled === true) {
2278
+ issues.push({
2279
+ severity: "info",
2280
+ check: "mcp-redundancy",
2281
+ ruleId: "disabled-server",
2282
+ line: server.line,
2283
+ message: `Server "${server.name}" is disabled \u2014 consider removing it if no longer needed`
2284
+ });
2285
+ }
2286
+ }
2287
+ }
2288
+ const projectConfigs = configs.filter((c) => c.scope === "project");
2289
+ const globalConfigs = configs.filter((c) => c.scope === "user" || c.scope === "global");
2290
+ for (const projectConfig of projectConfigs) {
2291
+ for (const projectServer of projectConfig.servers) {
2292
+ for (const globalConfig of globalConfigs) {
2293
+ const globalServer = globalConfig.servers.find((s) => s.name === projectServer.name);
2294
+ if (!globalServer) continue;
2295
+ const projectKey = JSON.stringify({
2296
+ command: projectServer.command,
2297
+ args: projectServer.args,
2298
+ url: projectServer.url,
2299
+ env: projectServer.env
2300
+ });
2301
+ const globalKey = JSON.stringify({
2302
+ command: globalServer.command,
2303
+ args: globalServer.args,
2304
+ url: globalServer.url,
2305
+ env: globalServer.env
2306
+ });
2307
+ if (projectKey === globalKey) {
2308
+ issues.push({
2309
+ severity: "info",
2310
+ check: "mcp-redundancy",
2311
+ ruleId: "identical-across-scopes",
2312
+ line: projectServer.line,
2313
+ message: `Server "${projectServer.name}" is identically configured in both ${projectConfig.relativePath} and ${globalConfig.relativePath}`
2314
+ });
2315
+ }
2316
+ }
2317
+ }
2318
+ }
2319
+ return issues;
2320
+ }
2321
+
2322
+ // src/core/audit.ts
2323
+ var ALL_CHECKS = [
2324
+ "paths",
2325
+ "commands",
2326
+ "staleness",
2327
+ "tokens",
2328
+ "redundancy",
2329
+ "contradictions",
2330
+ "frontmatter"
2331
+ ];
2332
+ var ALL_MCP_CHECKS = [
2333
+ "mcp-schema",
2334
+ "mcp-security",
2335
+ "mcp-commands",
2336
+ "mcp-deprecated",
2337
+ "mcp-env",
2338
+ "mcp-urls",
2339
+ "mcp-consistency",
2340
+ "mcp-redundancy"
2341
+ ];
2342
+ function hasMcpChecks(checks) {
2343
+ return checks.some((c) => c.startsWith("mcp-"));
2344
+ }
2345
+ async function runAudit(projectRoot, activeChecks, options = {}) {
2346
+ const fileResults = [];
2347
+ const shouldRunContextChecks = !options.mcpOnly;
2348
+ const shouldRunMcpChecks = options.mcp || options.mcpGlobal || options.mcpOnly || hasMcpChecks(activeChecks);
2349
+ if (shouldRunContextChecks) {
2350
+ const discovered = await scanForContextFiles(projectRoot, {
2351
+ depth: options.depth,
2352
+ extraPatterns: options.extraPatterns
2353
+ });
2354
+ const parsed = discovered.map((f) => parseContextFile(f));
2355
+ for (const file of parsed) {
2356
+ const checkPromises = [];
2357
+ if (activeChecks.includes("paths")) checkPromises.push(checkPaths(file, projectRoot));
2358
+ if (activeChecks.includes("commands")) checkPromises.push(checkCommands(file, projectRoot));
2359
+ if (activeChecks.includes("staleness")) checkPromises.push(checkStaleness(file, projectRoot));
2360
+ if (activeChecks.includes("tokens")) checkPromises.push(checkTokens(file, projectRoot));
2361
+ if (activeChecks.includes("redundancy"))
2362
+ checkPromises.push(checkRedundancy(file, projectRoot));
2363
+ if (activeChecks.includes("frontmatter"))
2364
+ checkPromises.push(checkFrontmatter(file, projectRoot));
2365
+ const results = await Promise.all(checkPromises);
2366
+ const issues = results.flat();
2367
+ fileResults.push({
2368
+ path: file.relativePath,
2369
+ isSymlink: file.isSymlink,
2370
+ symlinkTarget: file.symlinkTarget,
2371
+ tokens: file.totalTokens,
2372
+ lines: file.totalLines,
2373
+ issues
2374
+ });
2375
+ }
2376
+ if (activeChecks.includes("tokens")) {
2377
+ const aggIssue = checkAggregateTokens(
2378
+ fileResults.map((f) => ({ path: f.path, tokens: f.tokens }))
2379
+ );
2380
+ if (aggIssue && fileResults.length > 0) fileResults[0].issues.push(aggIssue);
2381
+ }
2382
+ if (activeChecks.includes("redundancy")) {
2383
+ const dupIssues = checkDuplicateContent(parsed);
2384
+ if (dupIssues.length > 0 && fileResults.length > 0) fileResults[0].issues.push(...dupIssues);
2385
+ }
2386
+ if (activeChecks.includes("contradictions")) {
2387
+ const contradictionIssues = checkContradictions(parsed);
2388
+ if (contradictionIssues.length > 0 && fileResults.length > 0)
2389
+ fileResults[0].issues.push(...contradictionIssues);
2390
+ }
2391
+ }
2392
+ if (shouldRunMcpChecks) {
2393
+ const mcpFiles = await scanForMcpConfigs(projectRoot);
2394
+ const mcpConfigs = mcpFiles.map(
2395
+ (f) => parseMcpConfig(f, projectRoot, "project")
2396
+ );
2397
+ if (options.mcpGlobal) {
2398
+ const globalFiles = await scanGlobalMcpConfigs();
2399
+ mcpConfigs.push(...globalFiles.map((f) => parseMcpConfig(f, projectRoot, "user")));
2400
+ }
2401
+ const activeMcpChecks = activeChecks.filter((c) => c.startsWith("mcp-"));
2402
+ const mcpChecksToRun = activeMcpChecks.length > 0 ? activeMcpChecks : options.mcp || options.mcpGlobal || options.mcpOnly ? ALL_MCP_CHECKS : [];
2403
+ for (const config of mcpConfigs) {
2404
+ const checkPromises = [];
2405
+ if (mcpChecksToRun.includes("mcp-schema"))
2406
+ checkPromises.push(checkMcpSchema(config, projectRoot));
2407
+ if (mcpChecksToRun.includes("mcp-security"))
2408
+ checkPromises.push(checkMcpSecurity(config, projectRoot));
2409
+ if (mcpChecksToRun.includes("mcp-commands"))
2410
+ checkPromises.push(checkMcpCommands(config, projectRoot));
2411
+ if (mcpChecksToRun.includes("mcp-deprecated"))
2412
+ checkPromises.push(checkMcpDeprecated(config, projectRoot));
2413
+ if (mcpChecksToRun.includes("mcp-env")) checkPromises.push(checkMcpEnv(config, projectRoot));
2414
+ if (mcpChecksToRun.includes("mcp-urls"))
2415
+ checkPromises.push(checkMcpUrls(config, projectRoot));
2416
+ const results = await Promise.all(checkPromises);
2417
+ const issues = results.flat();
2418
+ const lines = config.content.split("\n").length;
2419
+ fileResults.push({
2420
+ path: config.relativePath,
2421
+ isSymlink: false,
2422
+ tokens: 0,
2423
+ lines,
2424
+ issues
2425
+ });
2426
+ }
2427
+ if (mcpChecksToRun.includes("mcp-consistency")) {
2428
+ const consistencyIssues = await checkMcpConsistency(mcpConfigs);
2429
+ if (consistencyIssues.length > 0) {
2430
+ const firstMcpResult = fileResults.find(
2431
+ (f) => mcpConfigs.some((c) => c.relativePath === f.path)
2432
+ );
2433
+ if (firstMcpResult) {
2434
+ firstMcpResult.issues.push(...consistencyIssues);
2435
+ }
2436
+ }
2437
+ }
2438
+ if (mcpChecksToRun.includes("mcp-redundancy")) {
2439
+ const redundancyIssues = await checkMcpRedundancy(mcpConfigs);
2440
+ if (redundancyIssues.length > 0) {
2441
+ const firstMcpResult = fileResults.find(
2442
+ (f) => mcpConfigs.some((c) => c.relativePath === f.path)
2443
+ );
2444
+ if (firstMcpResult) {
2445
+ firstMcpResult.issues.push(...redundancyIssues);
2446
+ }
2447
+ }
2448
+ }
2449
+ }
2450
+ let estimatedWaste = 0;
2451
+ for (const fr of fileResults) {
2452
+ for (const issue of fr.issues) {
2453
+ if (issue.check === "redundancy" && issue.suggestion) {
2454
+ const tokenMatch = issue.suggestion.match(/~(\d+)\s+tokens/);
2455
+ if (tokenMatch) estimatedWaste += parseInt(tokenMatch[1], 10);
2456
+ }
2457
+ }
2458
+ }
2459
+ return {
2460
+ version: VERSION,
2461
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
2462
+ projectRoot,
2463
+ files: fileResults,
2464
+ summary: {
2465
+ errors: fileResults.reduce(
2466
+ (sum, f) => sum + f.issues.filter((i) => i.severity === "error").length,
2467
+ 0
2468
+ ),
2469
+ warnings: fileResults.reduce(
2470
+ (sum, f) => sum + f.issues.filter((i) => i.severity === "warning").length,
2471
+ 0
2472
+ ),
2473
+ info: fileResults.reduce(
2474
+ (sum, f) => sum + f.issues.filter((i) => i.severity === "info").length,
2475
+ 0
2476
+ ),
2477
+ totalTokens: fileResults.reduce((sum, f) => sum + f.tokens, 0),
2478
+ estimatedWaste
2479
+ }
2480
+ };
2481
+ }
2482
+
2483
+ // src/core/fixer.ts
2484
+ import * as fs5 from "fs";
2485
+ import chalk from "chalk";
2486
+ function applyFixes(result) {
2487
+ const fixesByFile = /* @__PURE__ */ new Map();
2488
+ for (const file of result.files) {
2489
+ for (const issue of file.issues) {
2490
+ if (issue.fix) {
2491
+ const existing = fixesByFile.get(issue.fix.file) || [];
2492
+ existing.push(issue.fix);
2493
+ fixesByFile.set(issue.fix.file, existing);
2494
+ }
2495
+ }
2496
+ }
2497
+ let totalFixes = 0;
2498
+ const filesModified = [];
2499
+ for (const [filePath, fixes] of fixesByFile) {
2500
+ const content = fs5.readFileSync(filePath, "utf-8");
2501
+ const lines = content.split("\n");
2502
+ let modified = false;
2503
+ const sortedFixes = [...fixes].sort((a, b) => b.line - a.line);
2504
+ for (const fix of sortedFixes) {
2505
+ const lineIdx = fix.line - 1;
2506
+ if (lineIdx < 0 || lineIdx >= lines.length) continue;
2507
+ const line = lines[lineIdx];
2508
+ if (line.includes(fix.oldText)) {
2509
+ lines[lineIdx] = line.replace(fix.oldText, fix.newText);
2510
+ modified = true;
2511
+ totalFixes++;
2512
+ console.log(
2513
+ chalk.green(" Fixed") + ` Line ${fix.line}: ${chalk.dim(fix.oldText)} ${chalk.dim("\u2192")} ${fix.newText}`
2514
+ );
2515
+ }
2516
+ }
2517
+ if (modified) {
2518
+ const newContent = lines.join("\n");
2519
+ if (filePath.endsWith(".json")) {
2520
+ try {
2521
+ JSON.parse(newContent);
2522
+ } catch {
2523
+ console.log(chalk.yellow(" Skipped") + ` ${filePath}: fix would produce invalid JSON`);
2524
+ continue;
2525
+ }
2526
+ }
2527
+ fs5.writeFileSync(filePath, newContent, "utf-8");
2528
+ filesModified.push(filePath);
2529
+ }
2530
+ }
2531
+ return { totalFixes, filesModified };
2532
+ }
2533
+
2534
+ export {
2535
+ fileExists,
2536
+ isDirectory,
2537
+ scanForContextFiles,
2538
+ freeEncoder,
2539
+ parseContextFile,
2540
+ resetGit,
2541
+ findRenames,
2542
+ resetPathsCache,
2543
+ setTokenThresholds,
2544
+ resetTokenThresholds,
2545
+ VERSION,
2546
+ ALL_CHECKS,
2547
+ ALL_MCP_CHECKS,
2548
+ runAudit,
2549
+ applyFixes
2550
+ };