@yawlabs/ctxlint 0.7.0 → 0.9.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.
@@ -1,3330 +0,0 @@
1
- import {
2
- __require
3
- } from "./chunk-MCKGQKYU.js";
4
-
5
- // src/mcp/server.ts
6
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
- import { z } from "zod";
9
-
10
- // src/core/scanner.ts
11
- import * as fs2 from "fs";
12
- import * as path2 from "path";
13
- import { glob } from "glob";
14
-
15
- // src/utils/fs.ts
16
- import * as fs from "fs";
17
- import * as path from "path";
18
- function loadPackageJson(projectRoot) {
19
- try {
20
- const content = fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8");
21
- return JSON.parse(content);
22
- } catch {
23
- return null;
24
- }
25
- }
26
- function fileExists(filePath) {
27
- try {
28
- fs.accessSync(filePath);
29
- return true;
30
- } catch {
31
- return false;
32
- }
33
- }
34
- function isDirectory(filePath) {
35
- try {
36
- return fs.statSync(filePath).isDirectory();
37
- } catch {
38
- return false;
39
- }
40
- }
41
- function isSymlink(filePath) {
42
- try {
43
- return fs.lstatSync(filePath).isSymbolicLink();
44
- } catch {
45
- return false;
46
- }
47
- }
48
- function readSymlinkTarget(filePath) {
49
- try {
50
- return fs.readlinkSync(filePath);
51
- } catch {
52
- return void 0;
53
- }
54
- }
55
- function readFileContent(filePath) {
56
- return fs.readFileSync(filePath, "utf-8");
57
- }
58
- var IGNORED_DIRS = /* @__PURE__ */ new Set([
59
- "node_modules",
60
- ".git",
61
- "dist",
62
- "build",
63
- "vendor",
64
- ".next",
65
- ".nuxt",
66
- "coverage",
67
- "__pycache__"
68
- ]);
69
- function getAllProjectFiles(projectRoot) {
70
- const files = [];
71
- function walk(dir, depth) {
72
- if (depth > 10) return;
73
- try {
74
- const entries = fs.readdirSync(dir, { withFileTypes: true });
75
- for (const entry of entries) {
76
- if (IGNORED_DIRS.has(entry.name)) continue;
77
- const fullPath = path.join(dir, entry.name);
78
- if (entry.isDirectory()) {
79
- walk(fullPath, depth + 1);
80
- } else {
81
- files.push(path.relative(projectRoot, fullPath));
82
- }
83
- }
84
- } catch {
85
- }
86
- }
87
- walk(projectRoot, 0);
88
- return files;
89
- }
90
-
91
- // src/core/scanner.ts
92
- var CONTEXT_FILE_PATTERNS = [
93
- // Claude Code
94
- "CLAUDE.md",
95
- "CLAUDE.local.md",
96
- ".claude/rules/*.md",
97
- // AGENTS.md (AAIF / Linux Foundation standard)
98
- "AGENTS.md",
99
- "AGENT.md",
100
- "AGENTS.override.md",
101
- // Cursor
102
- ".cursorrules",
103
- ".cursor/rules/*.md",
104
- ".cursor/rules/*.mdc",
105
- ".cursor/rules/*/RULE.md",
106
- // GitHub Copilot
107
- ".github/copilot-instructions.md",
108
- ".github/instructions/*.md",
109
- ".github/git-commit-instructions.md",
110
- // Windsurf
111
- ".windsurfrules",
112
- ".windsurf/rules/*.md",
113
- // Gemini CLI
114
- "GEMINI.md",
115
- // Cline
116
- ".clinerules",
117
- // Aider — note: .aiderules has no file extension; this is the intended format
118
- ".aiderules",
119
- // Aide / Codestory
120
- ".aide/rules/*.md",
121
- // Amazon Q Developer
122
- ".amazonq/rules/*.md",
123
- // Goose (Block)
124
- ".goose/instructions.md",
125
- ".goosehints",
126
- // JetBrains Junie
127
- ".junie/guidelines.md",
128
- ".junie/AGENTS.md",
129
- // JetBrains AI Assistant
130
- ".aiassistant/rules/*.md",
131
- // Continue
132
- ".continuerules",
133
- ".continue/rules/*.md",
134
- // Zed
135
- ".rules",
136
- // Replit
137
- "replit.md"
138
- ];
139
- var IGNORED_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor"]);
140
- async function scanForContextFiles(projectRoot, options = {}) {
141
- const maxDepth = options.depth ?? 2;
142
- const patterns = [...CONTEXT_FILE_PATTERNS, ...options.extraPatterns || []];
143
- const found = [];
144
- const seen = /* @__PURE__ */ new Set();
145
- const dirsToScan = [projectRoot];
146
- function collectDirs(dir, currentDepth) {
147
- if (currentDepth >= maxDepth) return;
148
- try {
149
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
150
- for (const entry of entries) {
151
- if (entry.isDirectory() && !IGNORED_DIRS2.has(entry.name) && !entry.name.startsWith(".")) {
152
- const fullPath = path2.join(dir, entry.name);
153
- dirsToScan.push(fullPath);
154
- collectDirs(fullPath, currentDepth + 1);
155
- }
156
- }
157
- } catch {
158
- }
159
- }
160
- collectDirs(projectRoot, 0);
161
- for (const dir of dirsToScan) {
162
- for (const pattern of patterns) {
163
- const matches = await glob(pattern, {
164
- cwd: dir,
165
- absolute: true,
166
- nodir: true,
167
- dot: true
168
- });
169
- for (const match of matches) {
170
- const normalized = path2.normalize(match);
171
- if (seen.has(normalized)) continue;
172
- seen.add(normalized);
173
- const relativePath = path2.relative(projectRoot, normalized);
174
- const symlink = isSymlink(normalized);
175
- const target = symlink ? readSymlinkTarget(normalized) : void 0;
176
- found.push({
177
- absolutePath: normalized,
178
- relativePath: relativePath.replace(/\\/g, "/"),
179
- isSymlink: symlink,
180
- symlinkTarget: target,
181
- type: "context"
182
- });
183
- }
184
- }
185
- }
186
- return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
187
- }
188
- var MCP_CONFIG_PATTERNS = [
189
- ".mcp.json",
190
- ".cursor/mcp.json",
191
- ".vscode/mcp.json",
192
- ".amazonq/mcp.json",
193
- ".continue/mcpServers/*.json"
194
- ];
195
- async function scanForMcpConfigs(projectRoot) {
196
- const found = [];
197
- const seen = /* @__PURE__ */ new Set();
198
- for (const pattern of MCP_CONFIG_PATTERNS) {
199
- const matches = await glob(pattern, {
200
- cwd: projectRoot,
201
- absolute: true,
202
- nodir: true,
203
- dot: true
204
- });
205
- for (const match of matches) {
206
- const normalized = path2.normalize(match);
207
- if (seen.has(normalized)) continue;
208
- seen.add(normalized);
209
- const relativePath = path2.relative(projectRoot, normalized);
210
- const symlink = isSymlink(normalized);
211
- const target = symlink ? readSymlinkTarget(normalized) : void 0;
212
- found.push({
213
- absolutePath: normalized,
214
- relativePath: relativePath.replace(/\\/g, "/"),
215
- isSymlink: symlink,
216
- symlinkTarget: target,
217
- type: "mcp-config"
218
- });
219
- }
220
- }
221
- return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
222
- }
223
- async function scanGlobalMcpConfigs() {
224
- const found = [];
225
- const seen = /* @__PURE__ */ new Set();
226
- const home2 = process.env.HOME || process.env.USERPROFILE || "";
227
- const globalPaths = [
228
- path2.join(home2, ".claude.json"),
229
- path2.join(home2, ".claude", "settings.json"),
230
- path2.join(home2, ".cursor", "mcp.json"),
231
- path2.join(home2, ".codeium", "windsurf", "mcp_config.json"),
232
- path2.join(home2, ".aws", "amazonq", "mcp.json")
233
- ];
234
- if (process.platform === "darwin") {
235
- globalPaths.push(
236
- path2.join(home2, "Library", "Application Support", "Claude", "claude_desktop_config.json")
237
- );
238
- } else if (process.platform === "win32") {
239
- const appData = process.env.APPDATA || path2.join(home2, "AppData", "Roaming");
240
- globalPaths.push(path2.join(appData, "Claude", "claude_desktop_config.json"));
241
- }
242
- for (const filePath of globalPaths) {
243
- const normalized = path2.normalize(filePath);
244
- if (seen.has(normalized)) continue;
245
- seen.add(normalized);
246
- try {
247
- fs2.accessSync(normalized);
248
- } catch {
249
- continue;
250
- }
251
- const symlink = isSymlink(normalized);
252
- const target = symlink ? readSymlinkTarget(normalized) : void 0;
253
- found.push({
254
- absolutePath: normalized,
255
- relativePath: "~/" + path2.relative(home2, normalized).replace(/\\/g, "/"),
256
- isSymlink: symlink,
257
- symlinkTarget: target,
258
- type: "mcp-config"
259
- });
260
- }
261
- return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
262
- }
263
-
264
- // src/utils/tokens.ts
265
- var encodingForModel = null;
266
- try {
267
- const tiktoken = await import("./tiktoken-MWTCLHI2.js");
268
- encodingForModel = tiktoken.encoding_for_model;
269
- } catch {
270
- }
271
- var encoder = null;
272
- function getEncoder() {
273
- if (!encoder && encodingForModel) {
274
- encoder = encodingForModel("gpt-4");
275
- }
276
- return encoder;
277
- }
278
- function countTokens(text) {
279
- const enc = getEncoder();
280
- if (!enc) {
281
- return Math.ceil(text.length / 4);
282
- }
283
- try {
284
- return enc.encode(text).length;
285
- } catch {
286
- return Math.ceil(text.length / 4);
287
- }
288
- }
289
- function freeEncoder() {
290
- if (encoder) {
291
- encoder.free();
292
- encoder = null;
293
- }
294
- }
295
-
296
- // src/core/parser.ts
297
- var PATH_PATTERN = /(?:^|[\s`"'(])((\.{0,2}\/)?(?:[\w@.-]+\/)+[\w.*-]+(?:\.\w+)?)(?=[\s`"'),;:]|$)/gm;
298
- var PATH_EXCLUDE = /^(https?:\/\/|ftp:\/\/|mailto:|n\/a|w\/o|I\/O|i\/o|e\.g\.|N\/A|\.deb\/|\.rpm[.\/]|\.tar[.\/]|\.zip[.\/])/i;
299
- var COMMAND_PREFIXES = /^\s*[\$>]\s+(.+)$/;
300
- var COMMON_COMMANDS = /^(npm\s+run|npx|pnpm|yarn|make|cargo|go\s+(run|build|test)|python|pytest|vitest|jest|bun|deno)\b/;
301
- function parseContextFile(file) {
302
- const content = readFileContent(file.absolutePath);
303
- const lines = content.split("\n");
304
- const sections = parseSections(lines);
305
- const paths = extractPathReferences(lines, sections);
306
- const commands = extractCommandReferences(lines, sections);
307
- return {
308
- filePath: file.absolutePath,
309
- relativePath: file.relativePath,
310
- isSymlink: file.isSymlink,
311
- symlinkTarget: file.symlinkTarget,
312
- totalTokens: countTokens(content),
313
- totalLines: lines.length,
314
- content,
315
- sections,
316
- references: {
317
- paths,
318
- commands
319
- }
320
- };
321
- }
322
- function parseSections(lines) {
323
- const sections = [];
324
- for (let i = 0; i < lines.length; i++) {
325
- const match = lines[i].match(/^(#{1,6})\s+(.+)/);
326
- if (match) {
327
- if (sections.length > 0) {
328
- const prev = sections[sections.length - 1];
329
- if (prev.endLine === -1) {
330
- prev.endLine = i - 1;
331
- }
332
- }
333
- sections.push({
334
- title: match[2].trim(),
335
- startLine: i + 1,
336
- // 1-indexed
337
- endLine: -1,
338
- level: match[1].length
339
- });
340
- }
341
- }
342
- if (sections.length > 0) {
343
- const last = sections[sections.length - 1];
344
- if (last.endLine === -1) {
345
- last.endLine = lines.length;
346
- }
347
- }
348
- return sections;
349
- }
350
- function getSectionForLine(line, sections) {
351
- for (let i = sections.length - 1; i >= 0; i--) {
352
- if (line >= sections[i].startLine) {
353
- return sections[i].title;
354
- }
355
- }
356
- return void 0;
357
- }
358
- function extractPathReferences(lines, sections) {
359
- const paths = [];
360
- let inCodeBlock = false;
361
- let codeBlockLang = "";
362
- for (let i = 0; i < lines.length; i++) {
363
- const line = lines[i];
364
- if (line.trimStart().startsWith("```")) {
365
- if (!inCodeBlock) {
366
- inCodeBlock = true;
367
- codeBlockLang = line.trimStart().slice(3).trim().toLowerCase();
368
- } else {
369
- inCodeBlock = false;
370
- codeBlockLang = "";
371
- }
372
- continue;
373
- }
374
- if (inCodeBlock && isExampleCodeBlock(codeBlockLang)) {
375
- continue;
376
- }
377
- PATH_PATTERN.lastIndex = 0;
378
- let match;
379
- while ((match = PATH_PATTERN.exec(line)) !== null) {
380
- const value = match[1];
381
- if (PATH_EXCLUDE.test(value)) continue;
382
- if (value.length < 3) continue;
383
- if (/^v?\d+\.\d+\//.test(value)) continue;
384
- const column = match.index + match[0].length - match[1].length + 1;
385
- paths.push({
386
- value,
387
- line: i + 1,
388
- // 1-indexed
389
- column,
390
- section: getSectionForLine(i + 1, sections)
391
- });
392
- }
393
- }
394
- return paths;
395
- }
396
- function isExampleCodeBlock(lang) {
397
- return [
398
- "javascript",
399
- "js",
400
- "typescript",
401
- "ts",
402
- "python",
403
- "py",
404
- "go",
405
- "rust",
406
- "java",
407
- "c",
408
- "cpp",
409
- "ruby",
410
- "php",
411
- "json",
412
- "yaml",
413
- "yml",
414
- "toml",
415
- "xml",
416
- "html",
417
- "css",
418
- "sql",
419
- "graphql",
420
- "jsx",
421
- "tsx"
422
- ].includes(lang);
423
- }
424
- function extractCommandReferences(lines, sections) {
425
- const commands = [];
426
- let inCodeBlock = false;
427
- let codeBlockLang = "";
428
- for (let i = 0; i < lines.length; i++) {
429
- const line = lines[i];
430
- if (line.trimStart().startsWith("```")) {
431
- if (!inCodeBlock) {
432
- inCodeBlock = true;
433
- codeBlockLang = line.trimStart().slice(3).trim().toLowerCase();
434
- } else {
435
- inCodeBlock = false;
436
- codeBlockLang = "";
437
- }
438
- continue;
439
- }
440
- const prefixMatch = line.match(COMMAND_PREFIXES);
441
- if (prefixMatch) {
442
- commands.push({
443
- value: prefixMatch[1].trim(),
444
- line: i + 1,
445
- column: prefixMatch.index + prefixMatch[0].length - prefixMatch[1].length + 1,
446
- section: getSectionForLine(i + 1, sections)
447
- });
448
- continue;
449
- }
450
- if (inCodeBlock && ["bash", "sh", "shell", "zsh", ""].includes(codeBlockLang)) {
451
- const trimmed = line.trim();
452
- if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("//")) {
453
- if (COMMON_COMMANDS.test(trimmed) || trimmed.startsWith("$") || trimmed.startsWith(">")) {
454
- const cmd = trimmed.replace(/^\s*[\$>]\s*/, "");
455
- if (cmd) {
456
- const cmdStart = line.indexOf(trimmed) + trimmed.indexOf(cmd);
457
- commands.push({
458
- value: cmd,
459
- line: i + 1,
460
- column: cmdStart + 1,
461
- section: getSectionForLine(i + 1, sections)
462
- });
463
- }
464
- }
465
- }
466
- continue;
467
- }
468
- const inlineMatches = line.matchAll(/`([^`]+)`/g);
469
- for (const m of inlineMatches) {
470
- const cmd = m[1].trim();
471
- if (COMMON_COMMANDS.test(cmd)) {
472
- commands.push({
473
- value: cmd,
474
- line: i + 1,
475
- column: (m.index ?? 0) + 2,
476
- section: getSectionForLine(i + 1, sections)
477
- });
478
- }
479
- }
480
- }
481
- return commands;
482
- }
483
-
484
- // src/core/mcp-parser.ts
485
- import { execSync } from "child_process";
486
- function parseMcpConfig(file, projectRoot, scopeOverride) {
487
- const content = readFileContent(file.absolutePath);
488
- const client = detectClient(file.relativePath);
489
- const scope = scopeOverride ?? detectScope(file.relativePath);
490
- const expectedRootKey = client === "vscode" ? "servers" : "mcpServers";
491
- const isGitTracked = checkGitTracked(file.absolutePath, projectRoot);
492
- const result = {
493
- filePath: file.absolutePath,
494
- relativePath: file.relativePath,
495
- client,
496
- scope,
497
- expectedRootKey,
498
- actualRootKey: null,
499
- servers: [],
500
- parseErrors: [],
501
- content,
502
- isGitTracked
503
- };
504
- let parsed;
505
- try {
506
- parsed = JSON.parse(content);
507
- } catch (err) {
508
- const message = err instanceof Error ? err.message : String(err);
509
- result.parseErrors.push(message);
510
- return result;
511
- }
512
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
513
- result.parseErrors.push("MCP config must be a JSON object");
514
- return result;
515
- }
516
- const rootKey = findRootKey(parsed);
517
- result.actualRootKey = rootKey;
518
- if (!rootKey) {
519
- return result;
520
- }
521
- const serversObj = parsed[rootKey];
522
- if (typeof serversObj !== "object" || serversObj === null || Array.isArray(serversObj)) {
523
- result.parseErrors.push(`"${rootKey}" must be an object`);
524
- return result;
525
- }
526
- const lines = content.split("\n");
527
- for (const [name, value] of Object.entries(serversObj)) {
528
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
529
- continue;
530
- }
531
- const raw = value;
532
- const line = findServerLine(lines, name);
533
- const transport2 = inferTransport(raw);
534
- const entry = {
535
- name,
536
- transport: transport2,
537
- line,
538
- raw
539
- };
540
- if (typeof raw.command === "string") entry.command = raw.command;
541
- if (Array.isArray(raw.args)) entry.args = raw.args.map(String);
542
- if (isStringRecord(raw.env)) entry.env = raw.env;
543
- if (typeof raw.url === "string") entry.url = raw.url;
544
- if (isStringRecord(raw.headers)) entry.headers = raw.headers;
545
- if (typeof raw.disabled === "boolean") entry.disabled = raw.disabled;
546
- if (Array.isArray(raw.autoApprove)) entry.autoApprove = raw.autoApprove.map(String);
547
- if (typeof raw.timeout === "number") entry.timeout = raw.timeout;
548
- if (typeof raw.oauth === "object" && raw.oauth !== null)
549
- entry.oauth = raw.oauth;
550
- if (typeof raw.headersHelper === "string") entry.headersHelper = raw.headersHelper;
551
- result.servers.push(entry);
552
- }
553
- return result;
554
- }
555
- function detectClient(relativePath) {
556
- const normalized = relativePath.replace(/\\/g, "/");
557
- if (normalized.includes(".vscode/")) return "vscode";
558
- if (normalized.includes(".cursor/")) return "cursor";
559
- if (normalized.includes(".amazonq/") || normalized.includes(".aws/amazonq/")) return "amazonq";
560
- if (normalized.includes(".continue/")) return "continue";
561
- if (normalized.includes(".codeium/windsurf/")) return "windsurf";
562
- if (normalized.includes("Claude/claude_desktop_config.json") || normalized.includes("Application Support/Claude/")) {
563
- return "claude-desktop";
564
- }
565
- return "claude-code";
566
- }
567
- function detectScope(_relativePath) {
568
- return "project";
569
- }
570
- function findRootKey(parsed) {
571
- if ("mcpServers" in parsed) return "mcpServers";
572
- if ("servers" in parsed) return "servers";
573
- return null;
574
- }
575
- function inferTransport(raw) {
576
- if (typeof raw.type === "string") {
577
- if (raw.type === "stdio" || raw.type === "http" || raw.type === "sse") {
578
- return raw.type;
579
- }
580
- return "unknown";
581
- }
582
- if ("command" in raw) return "stdio";
583
- if ("url" in raw) return "http";
584
- return "unknown";
585
- }
586
- function findServerLine(lines, serverName) {
587
- const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
588
- const pattern = new RegExp(`"${escaped}"\\s*:`);
589
- for (let i = 0; i < lines.length; i++) {
590
- if (pattern.test(lines[i])) {
591
- return i + 1;
592
- }
593
- }
594
- return 1;
595
- }
596
- function isStringRecord(value) {
597
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
598
- return Object.values(value).every((v) => typeof v === "string");
599
- }
600
- function checkGitTracked(filePath, projectRoot) {
601
- try {
602
- execSync(`git ls-files --error-unmatch "${filePath}"`, {
603
- cwd: projectRoot,
604
- stdio: "pipe"
605
- });
606
- return true;
607
- } catch {
608
- return false;
609
- }
610
- }
611
-
612
- // src/core/checks/paths.ts
613
- import * as path3 from "path";
614
- import levenshteinPkg from "fast-levenshtein";
615
- import { glob as glob2 } from "glob";
616
-
617
- // src/utils/git.ts
618
- import simpleGit from "simple-git";
619
- var gitInstance = null;
620
- var gitProjectRoot = null;
621
- function getGit(projectRoot) {
622
- if (!gitInstance || gitProjectRoot !== projectRoot) {
623
- gitInstance = simpleGit(projectRoot);
624
- gitProjectRoot = projectRoot;
625
- }
626
- return gitInstance;
627
- }
628
- function resetGit() {
629
- gitInstance = null;
630
- gitProjectRoot = null;
631
- }
632
- async function isGitRepo(projectRoot) {
633
- try {
634
- const git = simpleGit(projectRoot);
635
- await git.revparse(["--is-inside-work-tree"]);
636
- return true;
637
- } catch {
638
- return false;
639
- }
640
- }
641
- async function getFileLastModified(projectRoot, filePath) {
642
- try {
643
- const git = getGit(projectRoot);
644
- const log = await git.log({ file: filePath, maxCount: 1 });
645
- if (log.latest?.date) {
646
- return new Date(log.latest.date);
647
- }
648
- return null;
649
- } catch {
650
- return null;
651
- }
652
- }
653
- async function getCommitsSince(projectRoot, filePath, since) {
654
- try {
655
- const git = getGit(projectRoot);
656
- const log = await git.log({
657
- file: filePath,
658
- "--since": since.toISOString()
659
- });
660
- return log.total;
661
- } catch {
662
- return 0;
663
- }
664
- }
665
- async function findRenames(projectRoot, filePath) {
666
- try {
667
- const git = getGit(projectRoot);
668
- const result = await git.raw([
669
- "log",
670
- "--diff-filter=R",
671
- "--find-renames",
672
- "--name-status",
673
- "--format=%H %ai",
674
- "-10",
675
- "--",
676
- filePath
677
- ]);
678
- if (!result.trim()) return null;
679
- const lines = result.trim().split("\n");
680
- for (let i = 0; i < lines.length; i++) {
681
- const line = lines[i];
682
- if (line.startsWith("R")) {
683
- const parts = line.split(" ");
684
- if (parts.length >= 3) {
685
- const hashLine = lines[i - 1] || "";
686
- const hashMatch = hashLine.match(/^([a-f0-9]+)\s+(.+)/);
687
- const commitHash = hashMatch?.[1]?.substring(0, 7) || "unknown";
688
- const dateStr = hashMatch?.[2];
689
- const daysAgo = dateStr ? Math.floor((Date.now() - new Date(dateStr).getTime()) / (1e3 * 60 * 60 * 24)) : 0;
690
- return {
691
- oldPath: parts[1],
692
- newPath: parts[2],
693
- commitHash,
694
- daysAgo
695
- };
696
- }
697
- }
698
- }
699
- return null;
700
- } catch {
701
- return null;
702
- }
703
- }
704
-
705
- // src/core/checks/paths.ts
706
- var levenshtein = levenshteinPkg.get;
707
- var cachedProjectFiles = null;
708
- function getProjectFiles(projectRoot) {
709
- if (cachedProjectFiles?.root === projectRoot) return cachedProjectFiles.files;
710
- const files = getAllProjectFiles(projectRoot);
711
- cachedProjectFiles = { root: projectRoot, files };
712
- return files;
713
- }
714
- async function checkPaths(file, projectRoot) {
715
- const issues = [];
716
- const projectFiles = getProjectFiles(projectRoot);
717
- const contextDir = path3.dirname(file.filePath);
718
- for (const ref of file.references.paths) {
719
- const baseDir = ref.value.startsWith("./") || ref.value.startsWith("../") ? contextDir : projectRoot;
720
- const resolvedPath = path3.resolve(baseDir, ref.value);
721
- const normalizedRef = ref.value.replace(/\\/g, "/");
722
- if (normalizedRef.includes("*")) {
723
- const matches = await glob2(normalizedRef, { cwd: baseDir, nodir: false });
724
- if (matches.length === 0) {
725
- issues.push({
726
- severity: "error",
727
- check: "paths",
728
- line: ref.line,
729
- message: `${ref.value} matches no files`,
730
- suggestion: "Verify the glob pattern is correct"
731
- });
732
- }
733
- continue;
734
- }
735
- const isDir = normalizedRef.endsWith("/");
736
- if (isDir) {
737
- const dirPath = path3.resolve(baseDir, normalizedRef);
738
- if (!isDirectory(dirPath)) {
739
- issues.push({
740
- severity: "error",
741
- check: "paths",
742
- line: ref.line,
743
- message: `${ref.value} directory does not exist`
744
- });
745
- }
746
- continue;
747
- }
748
- if (fileExists(resolvedPath) || isDirectory(resolvedPath)) {
749
- continue;
750
- }
751
- let suggestion;
752
- let detail;
753
- let fixTarget;
754
- const rename = await findRenames(projectRoot, ref.value);
755
- if (rename) {
756
- fixTarget = rename.newPath;
757
- suggestion = `Did you mean ${rename.newPath}?`;
758
- detail = `Renamed ${rename.daysAgo} days ago in commit ${rename.commitHash}`;
759
- } else {
760
- const match = findClosestMatch(normalizedRef, projectFiles);
761
- if (match) {
762
- fixTarget = match;
763
- suggestion = `Did you mean ${match}?`;
764
- }
765
- }
766
- issues.push({
767
- severity: "error",
768
- check: "paths",
769
- line: ref.line,
770
- message: `${ref.value} does not exist`,
771
- suggestion,
772
- detail,
773
- fix: fixTarget ? { file: file.filePath, line: ref.line, oldText: ref.value, newText: fixTarget } : void 0
774
- });
775
- }
776
- return issues;
777
- }
778
- function findClosestMatch(target, files) {
779
- const targetNorm = target.replace(/\\/g, "/");
780
- const targetBase = path3.basename(targetNorm);
781
- let bestMatch = null;
782
- let bestDistance = Infinity;
783
- for (const file of files) {
784
- const fileNorm = file.replace(/\\/g, "/");
785
- if (path3.basename(fileNorm) === targetBase && fileNorm !== targetNorm) {
786
- const dist = levenshtein(targetNorm, fileNorm);
787
- if (dist < bestDistance) {
788
- bestDistance = dist;
789
- bestMatch = fileNorm;
790
- }
791
- }
792
- }
793
- if (!bestMatch) {
794
- for (const file of files) {
795
- const fileNorm = file.replace(/\\/g, "/");
796
- const dist = levenshtein(targetNorm, fileNorm);
797
- if (dist < bestDistance && dist <= Math.max(targetNorm.length * 0.4, 5)) {
798
- bestDistance = dist;
799
- bestMatch = fileNorm;
800
- }
801
- }
802
- }
803
- return bestMatch;
804
- }
805
-
806
- // src/core/checks/commands.ts
807
- import * as fs3 from "fs";
808
- import * as path4 from "path";
809
- var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
810
- var MAKE_PATTERN = /^make\s+(\S+)/;
811
- var NPX_PATTERN = /^npx\s+(\S+)/;
812
- async function checkCommands(file, projectRoot) {
813
- const issues = [];
814
- const pkgJson = loadPackageJson(projectRoot);
815
- const makefile = loadMakefile(projectRoot);
816
- for (const ref of file.references.commands) {
817
- const cmd = ref.value;
818
- const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
819
- if (scriptMatch && pkgJson) {
820
- const scriptName = scriptMatch[1];
821
- if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
822
- const available = Object.keys(pkgJson.scripts).join(", ");
823
- issues.push({
824
- severity: "error",
825
- check: "commands",
826
- line: ref.line,
827
- message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`,
828
- suggestion: available ? `Available scripts: ${available}` : void 0
829
- });
830
- }
831
- continue;
832
- }
833
- const shorthandMatch = cmd.match(
834
- /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/
835
- );
836
- if (shorthandMatch && pkgJson) {
837
- const scriptName = shorthandMatch[2];
838
- if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
839
- issues.push({
840
- severity: "error",
841
- check: "commands",
842
- line: ref.line,
843
- message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`
844
- });
845
- }
846
- continue;
847
- }
848
- const npxMatch = cmd.match(NPX_PATTERN);
849
- if (npxMatch && pkgJson) {
850
- const pkgName = npxMatch[1];
851
- if (pkgName.startsWith("-")) continue;
852
- const allDeps = {
853
- ...pkgJson.dependencies,
854
- ...pkgJson.devDependencies
855
- };
856
- if (!(pkgName in allDeps)) {
857
- const binPath = path4.join(projectRoot, "node_modules", ".bin", pkgName);
858
- try {
859
- fs3.accessSync(binPath);
860
- } catch {
861
- issues.push({
862
- severity: "warning",
863
- check: "commands",
864
- line: ref.line,
865
- message: `"${cmd}" \u2014 "${pkgName}" not found in dependencies`,
866
- suggestion: "If this is a global tool, consider adding it to devDependencies for reproducibility"
867
- });
868
- }
869
- }
870
- continue;
871
- }
872
- const makeMatch = cmd.match(MAKE_PATTERN);
873
- if (makeMatch) {
874
- const target = makeMatch[1];
875
- if (makefile && !hasMakeTarget(makefile, target)) {
876
- issues.push({
877
- severity: "error",
878
- check: "commands",
879
- line: ref.line,
880
- message: `"${cmd}" \u2014 target "${target}" not found in Makefile`
881
- });
882
- } else if (!makefile) {
883
- issues.push({
884
- severity: "error",
885
- check: "commands",
886
- line: ref.line,
887
- message: `"${cmd}" \u2014 no Makefile found in project`
888
- });
889
- }
890
- continue;
891
- }
892
- const toolMatch = cmd.match(/^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/);
893
- if (toolMatch && pkgJson) {
894
- const tool = toolMatch[1];
895
- const allDeps = {
896
- ...pkgJson.dependencies,
897
- ...pkgJson.devDependencies
898
- };
899
- if (!(tool in allDeps)) {
900
- const binPath = path4.join(projectRoot, "node_modules", ".bin", tool);
901
- try {
902
- fs3.accessSync(binPath);
903
- } catch {
904
- issues.push({
905
- severity: "warning",
906
- check: "commands",
907
- line: ref.line,
908
- message: `"${cmd}" \u2014 "${tool}" not found in dependencies or node_modules/.bin`
909
- });
910
- }
911
- }
912
- }
913
- }
914
- return issues;
915
- }
916
- function loadMakefile(projectRoot) {
917
- try {
918
- return fs3.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8");
919
- } catch {
920
- return null;
921
- }
922
- }
923
- function hasMakeTarget(makefile, target) {
924
- const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
925
- return pattern.test(makefile);
926
- }
927
-
928
- // src/core/checks/staleness.ts
929
- import * as path5 from "path";
930
- var WARNING_DAYS = 30;
931
- var INFO_DAYS = 14;
932
- async function checkStaleness(file, projectRoot) {
933
- const issues = [];
934
- if (!await isGitRepo(projectRoot)) {
935
- return issues;
936
- }
937
- const relativePath = path5.relative(projectRoot, file.filePath).replace(/\\/g, "/");
938
- const lastModified = await getFileLastModified(projectRoot, relativePath);
939
- if (!lastModified || isNaN(lastModified.getTime())) {
940
- return issues;
941
- }
942
- const daysSinceUpdate = Math.floor((Date.now() - lastModified.getTime()) / (1e3 * 60 * 60 * 24));
943
- if (daysSinceUpdate < INFO_DAYS) {
944
- return issues;
945
- }
946
- const referencedPaths = /* @__PURE__ */ new Set();
947
- for (const ref of file.references.paths) {
948
- const parts = ref.value.split("/");
949
- if (parts.length > 1) {
950
- referencedPaths.add(parts.slice(0, -1).join("/"));
951
- }
952
- referencedPaths.add(ref.value);
953
- }
954
- let totalCommits = 0;
955
- let mostActiveRef = "";
956
- let mostActiveCommits = 0;
957
- for (const refPath of referencedPaths) {
958
- const commits = await getCommitsSince(projectRoot, refPath, lastModified);
959
- totalCommits += commits;
960
- if (commits > mostActiveCommits) {
961
- mostActiveCommits = commits;
962
- mostActiveRef = refPath;
963
- }
964
- }
965
- if (totalCommits === 0) {
966
- return issues;
967
- }
968
- const severity = daysSinceUpdate >= WARNING_DAYS ? "warning" : "info";
969
- issues.push({
970
- severity,
971
- check: "staleness",
972
- line: 1,
973
- message: `Last updated ${daysSinceUpdate} days ago. ${mostActiveRef} has ${mostActiveCommits} commits since.`,
974
- suggestion: "Review and update this context file to reflect recent changes.",
975
- detail: `${totalCommits} total commits to referenced paths since last update.`
976
- });
977
- return issues;
978
- }
979
-
980
- // src/core/checks/tokens.ts
981
- var DEFAULT_THRESHOLDS = {
982
- info: 1e3,
983
- warning: 3e3,
984
- error: 8e3,
985
- aggregate: 5e3
986
- };
987
- var currentThresholds = DEFAULT_THRESHOLDS;
988
- async function checkTokens(file, _projectRoot) {
989
- const issues = [];
990
- const tokens = file.totalTokens;
991
- if (tokens >= currentThresholds.error) {
992
- issues.push({
993
- severity: "error",
994
- check: "tokens",
995
- line: 1,
996
- message: `${tokens.toLocaleString()} tokens \u2014 consumes significant context window space`,
997
- suggestion: "Consider splitting into focused sections or removing redundant content."
998
- });
999
- } else if (tokens >= currentThresholds.warning) {
1000
- issues.push({
1001
- severity: "warning",
1002
- check: "tokens",
1003
- line: 1,
1004
- message: `${tokens.toLocaleString()} tokens \u2014 large context file`,
1005
- suggestion: "Consider trimming \u2014 research shows diminishing returns past ~300 lines."
1006
- });
1007
- } else if (tokens >= currentThresholds.info) {
1008
- issues.push({
1009
- severity: "info",
1010
- check: "tokens",
1011
- line: 1,
1012
- message: `Uses ~${tokens.toLocaleString()} tokens per session`
1013
- });
1014
- }
1015
- return issues;
1016
- }
1017
- function checkAggregateTokens(files) {
1018
- const total = files.reduce((sum, f) => sum + f.tokens, 0);
1019
- if (total > currentThresholds.aggregate && files.length > 1) {
1020
- return {
1021
- severity: "warning",
1022
- check: "tokens",
1023
- line: 0,
1024
- message: `${files.length} context files consume ${total.toLocaleString()} tokens combined`,
1025
- suggestion: "Consider consolidating or trimming to reduce per-session context cost."
1026
- };
1027
- }
1028
- return null;
1029
- }
1030
-
1031
- // src/core/checks/redundancy.ts
1032
- import * as path6 from "path";
1033
- var PACKAGE_TECH_MAP = {
1034
- react: ["React", "react"],
1035
- "react-dom": ["React DOM", "ReactDOM"],
1036
- next: ["Next.js", "NextJS", "next.js"],
1037
- express: ["Express", "express.js"],
1038
- fastify: ["Fastify"],
1039
- typescript: ["TypeScript"],
1040
- vue: ["Vue", "Vue.js", "vue.js"],
1041
- angular: ["Angular"],
1042
- svelte: ["Svelte", "SvelteKit"],
1043
- tailwindcss: ["Tailwind", "TailwindCSS", "tailwind"],
1044
- prisma: ["Prisma"],
1045
- drizzle: ["Drizzle"],
1046
- "drizzle-orm": ["Drizzle"],
1047
- jest: ["Jest"],
1048
- vitest: ["Vitest"],
1049
- mocha: ["Mocha"],
1050
- eslint: ["ESLint"],
1051
- prettier: ["Prettier"],
1052
- webpack: ["Webpack"],
1053
- vite: ["Vite"],
1054
- esbuild: ["esbuild"],
1055
- tsup: ["tsup"],
1056
- rollup: ["Rollup"],
1057
- graphql: ["GraphQL"],
1058
- mongoose: ["Mongoose"],
1059
- sequelize: ["Sequelize"],
1060
- "socket.io": ["Socket.IO", "socket.io"],
1061
- redis: ["Redis"],
1062
- ioredis: ["Redis"],
1063
- postgres: ["PostgreSQL", "Postgres"],
1064
- pg: ["PostgreSQL", "Postgres"],
1065
- mysql2: ["MySQL"],
1066
- sqlite3: ["SQLite"],
1067
- "better-sqlite3": ["SQLite"],
1068
- zod: ["Zod"],
1069
- joi: ["Joi"],
1070
- axios: ["Axios"],
1071
- lodash: ["Lodash", "lodash"],
1072
- underscore: ["Underscore"],
1073
- moment: ["Moment", "moment.js"],
1074
- dayjs: ["Day.js", "dayjs"],
1075
- "date-fns": ["date-fns"],
1076
- docker: ["Docker"],
1077
- kubernetes: ["Kubernetes", "K8s"],
1078
- terraform: ["Terraform"],
1079
- storybook: ["Storybook"],
1080
- playwright: ["Playwright"],
1081
- cypress: ["Cypress"],
1082
- puppeteer: ["Puppeteer"]
1083
- };
1084
- function compilePatterns(allDeps) {
1085
- const compiled = [];
1086
- for (const [pkg, mentions] of Object.entries(PACKAGE_TECH_MAP)) {
1087
- if (!allDeps.has(pkg)) continue;
1088
- for (const mention of mentions) {
1089
- const escaped = escapeRegex(mention);
1090
- compiled.push({
1091
- pkg,
1092
- mention,
1093
- patterns: [
1094
- new RegExp(`\\b(?:use|using|built with|powered by|written in)\\s+${escaped}\\b`, "i"),
1095
- new RegExp(`\\bwe\\s+use\\s+${escaped}\\b`, "i"),
1096
- new RegExp(`\\b${escaped}\\s+(?:project|app|application|codebase)\\b`, "i"),
1097
- new RegExp(`\\bThis is a\\s+${escaped}\\b`, "i")
1098
- ]
1099
- });
1100
- }
1101
- }
1102
- return compiled;
1103
- }
1104
- async function checkRedundancy(file, projectRoot) {
1105
- const issues = [];
1106
- const pkgJson = loadPackageJson(projectRoot);
1107
- if (pkgJson) {
1108
- const allDeps = /* @__PURE__ */ new Set([
1109
- ...Object.keys(pkgJson.dependencies || {}),
1110
- ...Object.keys(pkgJson.devDependencies || {})
1111
- ]);
1112
- const compiledPatterns = compilePatterns(allDeps);
1113
- const lines2 = file.content.split("\n");
1114
- for (let i = 0; i < lines2.length; i++) {
1115
- const line = lines2[i];
1116
- for (const { pkg, mention, patterns } of compiledPatterns) {
1117
- let matched = false;
1118
- for (const pattern of patterns) {
1119
- if (pattern.test(line)) {
1120
- matched = true;
1121
- break;
1122
- }
1123
- }
1124
- if (matched) {
1125
- const wastedTokens = countTokens(line.trim());
1126
- issues.push({
1127
- severity: "info",
1128
- check: "redundancy",
1129
- line: i + 1,
1130
- message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : "devDependencies"} \u2014 agent can infer this`,
1131
- suggestion: `~${wastedTokens} tokens could be saved`
1132
- });
1133
- }
1134
- }
1135
- }
1136
- }
1137
- const lines = file.content.split("\n");
1138
- for (let i = 0; i < lines.length; i++) {
1139
- const line = lines[i];
1140
- const dirMatch = line.match(
1141
- /(?:are|go|live|found|located|stored)\s+(?:in|at|under)\s+[`"]?(\S+\/)[`"]?/i
1142
- );
1143
- if (dirMatch) {
1144
- const dir = dirMatch[1].replace(/[`"]/g, "");
1145
- const fullPath = path6.resolve(projectRoot, dir);
1146
- if (isDirectory(fullPath)) {
1147
- issues.push({
1148
- severity: "info",
1149
- check: "redundancy",
1150
- line: i + 1,
1151
- message: `Directory "${dir}" exists and is discoverable \u2014 agent can find this by listing files`,
1152
- suggestion: "Only keep if there is non-obvious context about this directory"
1153
- });
1154
- }
1155
- }
1156
- }
1157
- return issues;
1158
- }
1159
- function checkDuplicateContent(files) {
1160
- const issues = [];
1161
- for (let i = 0; i < files.length; i++) {
1162
- for (let j = i + 1; j < files.length; j++) {
1163
- const overlap = calculateLineOverlap(files[i].content, files[j].content);
1164
- if (overlap > 0.6) {
1165
- issues.push({
1166
- severity: "warning",
1167
- check: "redundancy",
1168
- line: 1,
1169
- message: `${files[i].relativePath} and ${files[j].relativePath} have ${Math.round(overlap * 100)}% content overlap`,
1170
- suggestion: "Consider consolidating into a single context file"
1171
- });
1172
- }
1173
- }
1174
- }
1175
- return issues;
1176
- }
1177
- function calculateLineOverlap(contentA, contentB) {
1178
- const linesA = new Set(
1179
- contentA.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
1180
- );
1181
- const linesB = new Set(
1182
- contentB.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
1183
- );
1184
- if (linesA.size === 0 || linesB.size === 0) return 0;
1185
- let overlap = 0;
1186
- for (const line of linesA) {
1187
- if (linesB.has(line)) overlap++;
1188
- }
1189
- return overlap / Math.min(linesA.size, linesB.size);
1190
- }
1191
- function escapeRegex(str) {
1192
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1193
- }
1194
-
1195
- // src/core/checks/contradictions.ts
1196
- var DIRECTIVE_CATEGORIES = [
1197
- {
1198
- name: "testing framework",
1199
- options: [
1200
- {
1201
- label: "Jest",
1202
- patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest.*with\s+jest\b/i]
1203
- },
1204
- {
1205
- label: "Vitest",
1206
- patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest.*with\s+vitest\b/i]
1207
- },
1208
- {
1209
- label: "Mocha",
1210
- patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest.*with\s+mocha\b/i]
1211
- },
1212
- {
1213
- label: "pytest",
1214
- patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest.*with\s+pytest\b/i]
1215
- },
1216
- {
1217
- label: "Playwright",
1218
- patterns: [/\buse\s+playwright\b/i, /\bplaywright\s+for\s+(?:e2e|test)/i]
1219
- },
1220
- { label: "Cypress", patterns: [/\buse\s+cypress\b/i, /\bcypress\s+for\s+(?:e2e|test)/i] }
1221
- ]
1222
- },
1223
- {
1224
- name: "package manager",
1225
- options: [
1226
- {
1227
- label: "npm",
1228
- patterns: [
1229
- /\buse\s+npm\b/i,
1230
- /\bnpm\s+as\s+(?:the\s+)?package\s+manager/i,
1231
- /\balways\s+use\s+npm\b/i
1232
- ]
1233
- },
1234
- {
1235
- label: "pnpm",
1236
- patterns: [
1237
- /\buse\s+pnpm\b/i,
1238
- /\bpnpm\s+as\s+(?:the\s+)?package\s+manager/i,
1239
- /\balways\s+use\s+pnpm\b/i
1240
- ]
1241
- },
1242
- {
1243
- label: "yarn",
1244
- patterns: [
1245
- /\buse\s+yarn\b/i,
1246
- /\byarn\s+as\s+(?:the\s+)?package\s+manager/i,
1247
- /\balways\s+use\s+yarn\b/i
1248
- ]
1249
- },
1250
- {
1251
- label: "bun",
1252
- patterns: [
1253
- /\buse\s+bun\b/i,
1254
- /\bbun\s+as\s+(?:the\s+)?package\s+manager/i,
1255
- /\balways\s+use\s+bun\b/i
1256
- ]
1257
- }
1258
- ]
1259
- },
1260
- {
1261
- name: "indentation style",
1262
- options: [
1263
- {
1264
- label: "tabs",
1265
- patterns: [/\buse\s+tabs\b/i, /\btab\s+indentation\b/i, /\bindent\s+with\s+tabs\b/i]
1266
- },
1267
- {
1268
- label: "2 spaces",
1269
- patterns: [
1270
- /\b2[\s-]?space\s+indent/i,
1271
- /\bindent\s+with\s+2\s+spaces/i,
1272
- /\b2[\s-]?space\s+tabs?\b/i
1273
- ]
1274
- },
1275
- {
1276
- label: "4 spaces",
1277
- patterns: [
1278
- /\b4[\s-]?space\s+indent/i,
1279
- /\bindent\s+with\s+4\s+spaces/i,
1280
- /\b4[\s-]?space\s+tabs?\b/i
1281
- ]
1282
- }
1283
- ]
1284
- },
1285
- {
1286
- name: "semicolons",
1287
- options: [
1288
- {
1289
- label: "semicolons",
1290
- patterns: [
1291
- /\buse\s+semicolons\b/i,
1292
- /\balways\s+(?:use\s+)?semicolons\b/i,
1293
- /\bsemicolons:\s*(?:true|yes)\b/i
1294
- ]
1295
- },
1296
- {
1297
- label: "no semicolons",
1298
- patterns: [
1299
- /\bno\s+semicolons\b/i,
1300
- /\bavoid\s+semicolons\b/i,
1301
- /\bomit\s+semicolons\b/i,
1302
- /\bsemicolons:\s*(?:false|no)\b/i
1303
- ]
1304
- }
1305
- ]
1306
- },
1307
- {
1308
- name: "quote style",
1309
- options: [
1310
- {
1311
- label: "single quotes",
1312
- patterns: [
1313
- /\bsingle\s+quotes?\b/i,
1314
- /\buse\s+(?:single\s+)?['']single['']?\s+quotes?\b/i,
1315
- /\bprefer\s+single\s+quotes?\b/i
1316
- ]
1317
- },
1318
- {
1319
- label: "double quotes",
1320
- patterns: [
1321
- /\bdouble\s+quotes?\b/i,
1322
- /\buse\s+(?:double\s+)?[""]double[""]?\s+quotes?\b/i,
1323
- /\bprefer\s+double\s+quotes?\b/i
1324
- ]
1325
- }
1326
- ]
1327
- },
1328
- {
1329
- name: "naming convention",
1330
- options: [
1331
- {
1332
- label: "camelCase",
1333
- patterns: [/\bcamelCase\b/, /\bcamel[\s-]?case\s+(?:for|naming|convention)/i]
1334
- },
1335
- {
1336
- label: "snake_case",
1337
- patterns: [/\bsnake_case\b/, /\bsnake[\s-]?case\s+(?:for|naming|convention)/i]
1338
- },
1339
- {
1340
- label: "PascalCase",
1341
- patterns: [/\bPascalCase\b/, /\bpascal[\s-]?case\s+(?:for|naming|convention)/i]
1342
- },
1343
- {
1344
- label: "kebab-case",
1345
- patterns: [/\bkebab-case\b/, /\bkebab[\s-]?case\s+(?:for|naming|convention)/i]
1346
- }
1347
- ]
1348
- },
1349
- {
1350
- name: "CSS approach",
1351
- options: [
1352
- { label: "Tailwind", patterns: [/\buse\s+tailwind/i, /\btailwind\s+for\s+styl/i] },
1353
- {
1354
- label: "CSS Modules",
1355
- patterns: [/\buse\s+css\s+modules\b/i, /\bcss\s+modules\s+for\s+styl/i]
1356
- },
1357
- {
1358
- label: "styled-components",
1359
- patterns: [/\buse\s+styled[\s-]?components\b/i, /\bstyled[\s-]?components\s+for\s+styl/i]
1360
- },
1361
- { label: "CSS-in-JS", patterns: [/\buse\s+css[\s-]?in[\s-]?js\b/i] }
1362
- ]
1363
- },
1364
- {
1365
- name: "state management",
1366
- options: [
1367
- { label: "Redux", patterns: [/\buse\s+redux\b/i, /\bredux\s+for\s+state/i] },
1368
- { label: "Zustand", patterns: [/\buse\s+zustand\b/i, /\bzustand\s+for\s+state/i] },
1369
- { label: "MobX", patterns: [/\buse\s+mobx\b/i, /\bmobx\s+for\s+state/i] },
1370
- { label: "Jotai", patterns: [/\buse\s+jotai\b/i, /\bjotai\s+for\s+state/i] },
1371
- { label: "Recoil", patterns: [/\buse\s+recoil\b/i, /\brecoil\s+for\s+state/i] }
1372
- ]
1373
- }
1374
- ];
1375
- function detectDirectives(file) {
1376
- const directives = [];
1377
- const lines = file.content.split("\n");
1378
- for (let i = 0; i < lines.length; i++) {
1379
- const line = lines[i];
1380
- for (const category of DIRECTIVE_CATEGORIES) {
1381
- for (const option of category.options) {
1382
- for (const pattern of option.patterns) {
1383
- if (pattern.test(line)) {
1384
- directives.push({
1385
- file: file.relativePath,
1386
- category: category.name,
1387
- label: option.label,
1388
- line: i + 1,
1389
- text: line.trim()
1390
- });
1391
- break;
1392
- }
1393
- }
1394
- }
1395
- }
1396
- }
1397
- return directives;
1398
- }
1399
- function checkContradictions(files) {
1400
- if (files.length < 2) return [];
1401
- const issues = [];
1402
- const allDirectives = [];
1403
- for (const file of files) {
1404
- allDirectives.push(...detectDirectives(file));
1405
- }
1406
- const byCategory = /* @__PURE__ */ new Map();
1407
- for (const d of allDirectives) {
1408
- const existing = byCategory.get(d.category) || [];
1409
- existing.push(d);
1410
- byCategory.set(d.category, existing);
1411
- }
1412
- for (const [category, directives] of byCategory) {
1413
- const byFile = /* @__PURE__ */ new Map();
1414
- for (const d of directives) {
1415
- const existing = byFile.get(d.file) || [];
1416
- existing.push(d);
1417
- byFile.set(d.file, existing);
1418
- }
1419
- const labels = new Set(directives.map((d) => d.label));
1420
- if (labels.size <= 1) continue;
1421
- const fileLabels = /* @__PURE__ */ new Map();
1422
- for (const d of directives) {
1423
- const existing = fileLabels.get(d.file) || /* @__PURE__ */ new Set();
1424
- existing.add(d.label);
1425
- fileLabels.set(d.file, existing);
1426
- }
1427
- const fileEntries = [...fileLabels.entries()];
1428
- for (let i = 0; i < fileEntries.length; i++) {
1429
- for (let j = i + 1; j < fileEntries.length; j++) {
1430
- const [fileA, labelsA] = fileEntries[i];
1431
- const [fileB, labelsB] = fileEntries[j];
1432
- for (const labelA of labelsA) {
1433
- for (const labelB of labelsB) {
1434
- if (labelA !== labelB) {
1435
- const directiveA = directives.find((d) => d.file === fileA && d.label === labelA);
1436
- const directiveB = directives.find((d) => d.file === fileB && d.label === labelB);
1437
- issues.push({
1438
- severity: "warning",
1439
- check: "contradictions",
1440
- line: directiveA.line,
1441
- message: `${category} conflict: "${directiveA.label}" in ${fileA} vs "${directiveB.label}" in ${fileB}`,
1442
- suggestion: `Align on one ${category} across all context files`,
1443
- detail: `${fileA}:${directiveA.line} says "${directiveA.text}" but ${fileB}:${directiveB.line} says "${directiveB.text}"`
1444
- });
1445
- }
1446
- }
1447
- }
1448
- }
1449
- }
1450
- }
1451
- return issues;
1452
- }
1453
-
1454
- // src/core/checks/frontmatter.ts
1455
- function parseFrontmatter(content) {
1456
- const lines = content.split("\n");
1457
- if (lines[0]?.trim() !== "---") {
1458
- return { found: false, fields: {}, endLine: 0 };
1459
- }
1460
- const fields = {};
1461
- let endLine = 0;
1462
- for (let i = 1; i < lines.length; i++) {
1463
- const line = lines[i].trim();
1464
- if (line === "---") {
1465
- endLine = i + 1;
1466
- break;
1467
- }
1468
- const match = line.match(/^(\w+)\s*:\s*(.*)$/);
1469
- if (match) {
1470
- fields[match[1]] = match[2].trim();
1471
- }
1472
- }
1473
- if (endLine === 0) {
1474
- return { found: true, fields, endLine: lines.length };
1475
- }
1476
- return { found: true, fields, endLine };
1477
- }
1478
- function isCursorMdc(file) {
1479
- return file.relativePath.endsWith(".mdc");
1480
- }
1481
- function isCopilotInstructions(file) {
1482
- return file.relativePath.includes(".github/instructions/") && file.relativePath.endsWith(".md");
1483
- }
1484
- function isWindsurfRule(file) {
1485
- return file.relativePath.includes(".windsurf/rules/") && file.relativePath.endsWith(".md");
1486
- }
1487
- var VALID_WINDSURF_TRIGGERS = ["always_on", "glob", "manual", "model"];
1488
- async function checkFrontmatter(file, _projectRoot) {
1489
- const issues = [];
1490
- if (isCursorMdc(file)) {
1491
- issues.push(...validateCursorMdc(file));
1492
- } else if (isCopilotInstructions(file)) {
1493
- issues.push(...validateCopilotInstructions(file));
1494
- } else if (isWindsurfRule(file)) {
1495
- issues.push(...validateWindsurfRule(file));
1496
- }
1497
- return issues;
1498
- }
1499
- function validateCursorMdc(file) {
1500
- const issues = [];
1501
- const fm = parseFrontmatter(file.content);
1502
- if (!fm.found) {
1503
- issues.push({
1504
- severity: "warning",
1505
- check: "frontmatter",
1506
- line: 1,
1507
- message: "Cursor .mdc file is missing frontmatter",
1508
- suggestion: "Add YAML frontmatter with description, globs, and alwaysApply fields"
1509
- });
1510
- return issues;
1511
- }
1512
- if (!fm.fields["description"]) {
1513
- issues.push({
1514
- severity: "warning",
1515
- check: "frontmatter",
1516
- line: 1,
1517
- message: 'Missing "description" field in Cursor .mdc frontmatter',
1518
- suggestion: "Add a description so Cursor knows when to apply this rule"
1519
- });
1520
- }
1521
- if (!("alwaysApply" in fm.fields) && !("globs" in fm.fields)) {
1522
- issues.push({
1523
- severity: "info",
1524
- check: "frontmatter",
1525
- line: 1,
1526
- message: 'No "alwaysApply" or "globs" field \u2014 rule may not be applied automatically',
1527
- suggestion: "Set alwaysApply: true or specify globs for targeted activation"
1528
- });
1529
- }
1530
- if ("alwaysApply" in fm.fields) {
1531
- const val = fm.fields["alwaysApply"].toLowerCase();
1532
- if (!["true", "false"].includes(val)) {
1533
- issues.push({
1534
- severity: "error",
1535
- check: "frontmatter",
1536
- line: 1,
1537
- message: `Invalid alwaysApply value: "${fm.fields["alwaysApply"]}"`,
1538
- suggestion: "alwaysApply must be true or false"
1539
- });
1540
- }
1541
- }
1542
- if ("globs" in fm.fields) {
1543
- const val = fm.fields["globs"];
1544
- if (val && !val.startsWith("[") && !val.startsWith('"') && !val.includes("*") && !val.includes("/")) {
1545
- issues.push({
1546
- severity: "warning",
1547
- check: "frontmatter",
1548
- line: 1,
1549
- message: `Possibly invalid globs value: "${val}"`,
1550
- suggestion: 'globs should be a glob pattern like "src/**/*.ts" or an array like ["*.ts", "*.tsx"]'
1551
- });
1552
- }
1553
- }
1554
- return issues;
1555
- }
1556
- function validateCopilotInstructions(file) {
1557
- const issues = [];
1558
- const fm = parseFrontmatter(file.content);
1559
- if (!fm.found) {
1560
- issues.push({
1561
- severity: "info",
1562
- check: "frontmatter",
1563
- line: 1,
1564
- message: "Copilot instructions file has no frontmatter",
1565
- suggestion: "Add applyTo frontmatter to target specific file patterns"
1566
- });
1567
- return issues;
1568
- }
1569
- if (!fm.fields["applyTo"]) {
1570
- issues.push({
1571
- severity: "warning",
1572
- check: "frontmatter",
1573
- line: 1,
1574
- message: 'Missing "applyTo" field in Copilot instructions frontmatter',
1575
- suggestion: 'Add applyTo to specify which files this instruction applies to (e.g., applyTo: "**/*.ts")'
1576
- });
1577
- }
1578
- return issues;
1579
- }
1580
- function validateWindsurfRule(file) {
1581
- const issues = [];
1582
- const fm = parseFrontmatter(file.content);
1583
- if (!fm.found) {
1584
- issues.push({
1585
- severity: "info",
1586
- check: "frontmatter",
1587
- line: 1,
1588
- message: "Windsurf rule file has no frontmatter",
1589
- suggestion: "Add YAML frontmatter with a trigger field (always_on, glob, manual, model)"
1590
- });
1591
- return issues;
1592
- }
1593
- if (!fm.fields["trigger"]) {
1594
- issues.push({
1595
- severity: "warning",
1596
- check: "frontmatter",
1597
- line: 1,
1598
- message: 'Missing "trigger" field in Windsurf rule frontmatter',
1599
- suggestion: `Set trigger to one of: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
1600
- });
1601
- } else {
1602
- const trigger = fm.fields["trigger"].replace(/['"]/g, "");
1603
- if (!VALID_WINDSURF_TRIGGERS.includes(trigger)) {
1604
- issues.push({
1605
- severity: "error",
1606
- check: "frontmatter",
1607
- line: 1,
1608
- message: `Invalid trigger value: "${trigger}"`,
1609
- suggestion: `Valid triggers: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
1610
- });
1611
- }
1612
- }
1613
- return issues;
1614
- }
1615
-
1616
- // src/core/checks/mcp/schema.ts
1617
- async function checkMcpSchema(config, _projectRoot) {
1618
- const issues = [];
1619
- if (config.parseErrors.length > 0) {
1620
- for (const err of config.parseErrors) {
1621
- issues.push({
1622
- severity: "error",
1623
- check: "mcp-schema",
1624
- ruleId: "invalid-json",
1625
- line: 1,
1626
- message: `MCP config is not valid JSON: ${err}`
1627
- });
1628
- }
1629
- return issues;
1630
- }
1631
- if (!config.actualRootKey) {
1632
- issues.push({
1633
- severity: "error",
1634
- check: "mcp-schema",
1635
- ruleId: "missing-root-key",
1636
- line: 1,
1637
- message: `MCP config has no "${config.expectedRootKey}" key`
1638
- });
1639
- return issues;
1640
- }
1641
- if (config.actualRootKey !== config.expectedRootKey) {
1642
- const line = findKeyLine(config.content, config.actualRootKey);
1643
- issues.push({
1644
- severity: "error",
1645
- check: "mcp-schema",
1646
- ruleId: "wrong-root-key",
1647
- line,
1648
- message: `${config.relativePath} must use "${config.expectedRootKey}" as root key, not "${config.actualRootKey}"`,
1649
- fix: {
1650
- file: config.filePath,
1651
- line,
1652
- oldText: `"${config.actualRootKey}"`,
1653
- newText: `"${config.expectedRootKey}"`
1654
- }
1655
- });
1656
- }
1657
- if (config.servers.length === 0 && config.actualRootKey) {
1658
- issues.push({
1659
- severity: "info",
1660
- check: "mcp-schema",
1661
- ruleId: "empty-servers",
1662
- line: 1,
1663
- message: "MCP config has no server entries"
1664
- });
1665
- return issues;
1666
- }
1667
- for (const server2 of config.servers) {
1668
- if (!server2.name) {
1669
- issues.push({
1670
- severity: "error",
1671
- check: "mcp-schema",
1672
- ruleId: "no-name-field",
1673
- line: server2.line,
1674
- message: "Server name cannot be empty"
1675
- });
1676
- continue;
1677
- }
1678
- if (server2.transport === "unknown") {
1679
- const typeVal = server2.raw.type;
1680
- if (typeof typeVal === "string") {
1681
- issues.push({
1682
- severity: "warning",
1683
- check: "mcp-schema",
1684
- ruleId: "unknown-transport",
1685
- line: server2.line,
1686
- message: `Server "${server2.name}" has unknown transport type "${typeVal}"`
1687
- });
1688
- }
1689
- }
1690
- if (server2.command && server2.url) {
1691
- issues.push({
1692
- severity: "warning",
1693
- check: "mcp-schema",
1694
- ruleId: "ambiguous-transport",
1695
- line: server2.line,
1696
- message: `Server "${server2.name}" has both "command" and "url" \u2014 transport is ambiguous`
1697
- });
1698
- }
1699
- if (server2.transport === "stdio" && !server2.command) {
1700
- issues.push({
1701
- severity: "error",
1702
- check: "mcp-schema",
1703
- ruleId: "missing-command",
1704
- line: server2.line,
1705
- message: `Server "${server2.name}" has no "command" field`
1706
- });
1707
- }
1708
- if ((server2.transport === "http" || server2.transport === "sse") && !server2.url) {
1709
- issues.push({
1710
- severity: "error",
1711
- check: "mcp-schema",
1712
- ruleId: "missing-url",
1713
- line: server2.line,
1714
- message: `Server "${server2.name}" has no "url" field`
1715
- });
1716
- }
1717
- }
1718
- return issues;
1719
- }
1720
- function findKeyLine(content, key) {
1721
- const lines = content.split("\n");
1722
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1723
- const pattern = new RegExp(`"${escaped}"\\s*:`);
1724
- for (let i = 0; i < lines.length; i++) {
1725
- if (pattern.test(lines[i])) {
1726
- return i + 1;
1727
- }
1728
- }
1729
- return 1;
1730
- }
1731
-
1732
- // src/core/checks/mcp/security.ts
1733
- var API_KEY_PATTERNS = [
1734
- /sk-[a-zA-Z0-9]{20,}/,
1735
- // OpenAI / generic
1736
- /ghp_[a-zA-Z0-9]{36}/,
1737
- // GitHub PAT
1738
- /ghu_[a-zA-Z0-9]{36}/,
1739
- // GitHub user token
1740
- /github_pat_[a-zA-Z0-9_]{80,}/,
1741
- // GitHub fine-grained PAT
1742
- /xoxb-[0-9]{10,}/,
1743
- // Slack bot
1744
- /xoxp-[0-9]{10,}/,
1745
- // Slack user
1746
- /AKIA[0-9A-Z]{16}/,
1747
- // AWS access key
1748
- /AGE-SECRET-KEY-1[a-zA-Z0-9]+/,
1749
- // age encryption key
1750
- /glpat-[a-zA-Z0-9_\-]{20}/,
1751
- // GitLab PAT
1752
- /sq0atp-[a-zA-Z0-9_\-]{22}/
1753
- // Square
1754
- ];
1755
- var ENV_VAR_REF = /\$\{[^}]+\}/;
1756
- var HIGH_ENTROPY_PATTERN = /^[A-Za-z0-9+/=_-]{21,}$/;
1757
- var URL_SECRET_PARAMS = /[?&](key|token|api_key|apikey|secret|password|access_token)=/i;
1758
- function isEnvVarRef(value) {
1759
- return ENV_VAR_REF.test(value);
1760
- }
1761
- function isKnownApiKey(value) {
1762
- return API_KEY_PATTERNS.some((p) => p.test(value));
1763
- }
1764
- function isHighEntropySecret(value) {
1765
- if (isEnvVarRef(value)) return false;
1766
- return HIGH_ENTROPY_PATTERN.test(value);
1767
- }
1768
- function deriveEnvVarName(serverName, suffix) {
1769
- return serverName.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase().replace(/^_|_$/g, "") + "_" + suffix;
1770
- }
1771
- async function checkMcpSecurity(config, _projectRoot) {
1772
- const issues = [];
1773
- if (!config.isGitTracked) return issues;
1774
- if (config.parseErrors.length > 0) return issues;
1775
- for (const server2 of config.servers) {
1776
- if (server2.headers) {
1777
- for (const [headerName, headerValue] of Object.entries(server2.headers)) {
1778
- if (headerName.toLowerCase() === "authorization") {
1779
- const bearerMatch = headerValue.match(/^Bearer\s+(.+)$/i);
1780
- if (bearerMatch) {
1781
- const token = bearerMatch[1];
1782
- if (!isEnvVarRef(token)) {
1783
- const envVar = deriveEnvVarName(server2.name, "API_KEY");
1784
- issues.push({
1785
- severity: "error",
1786
- check: "mcp-security",
1787
- ruleId: "hardcoded-bearer",
1788
- line: server2.line,
1789
- message: `Server "${server2.name}" has a hardcoded Bearer token in a git-tracked file`,
1790
- fix: {
1791
- file: config.filePath,
1792
- line: server2.line,
1793
- oldText: `Bearer ${token}`,
1794
- newText: `Bearer \${${envVar}}`
1795
- }
1796
- });
1797
- }
1798
- }
1799
- }
1800
- if (!isEnvVarRef(headerValue) && isKnownApiKey(headerValue)) {
1801
- issues.push({
1802
- severity: "error",
1803
- check: "mcp-security",
1804
- ruleId: "hardcoded-api-key",
1805
- line: server2.line,
1806
- message: `Server "${server2.name}" has a hardcoded API key in a git-tracked file`
1807
- });
1808
- }
1809
- }
1810
- }
1811
- if (server2.env) {
1812
- for (const envValue of Object.values(server2.env)) {
1813
- if (!isEnvVarRef(envValue) && (isKnownApiKey(envValue) || isHighEntropySecret(envValue))) {
1814
- const envVar = deriveEnvVarName(server2.name, "API_KEY");
1815
- issues.push({
1816
- severity: "error",
1817
- check: "mcp-security",
1818
- ruleId: "hardcoded-api-key",
1819
- line: server2.line,
1820
- message: `Server "${server2.name}" has a hardcoded API key in a git-tracked file`,
1821
- fix: {
1822
- file: config.filePath,
1823
- line: server2.line,
1824
- oldText: envValue,
1825
- newText: `\${${envVar}}`
1826
- }
1827
- });
1828
- }
1829
- }
1830
- }
1831
- if (server2.url && !isEnvVarRef(server2.url) && URL_SECRET_PARAMS.test(server2.url)) {
1832
- issues.push({
1833
- severity: "error",
1834
- check: "mcp-security",
1835
- ruleId: "secret-in-url",
1836
- line: server2.line,
1837
- message: `Server "${server2.name}" has a secret in the URL query string`
1838
- });
1839
- }
1840
- if (server2.url) {
1841
- try {
1842
- if (!isEnvVarRef(server2.url)) {
1843
- const parsed = new URL(server2.url);
1844
- if (parsed.protocol === "http:" && parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1" && parsed.hostname !== "::1") {
1845
- issues.push({
1846
- severity: "warning",
1847
- check: "mcp-security",
1848
- ruleId: "http-no-tls",
1849
- line: server2.line,
1850
- message: `Server "${server2.name}" uses HTTP without TLS`
1851
- });
1852
- }
1853
- }
1854
- } catch {
1855
- }
1856
- }
1857
- }
1858
- return issues;
1859
- }
1860
-
1861
- // src/core/checks/mcp/commands.ts
1862
- import * as fs4 from "fs";
1863
- import * as path7 from "path";
1864
- var LOCAL_PATH_PATTERN = /^\.\.?\//;
1865
- var FILE_PATH_PATTERN = /^[^-].*\/.*\.\w+$/;
1866
- async function checkMcpCommands(config, projectRoot) {
1867
- const issues = [];
1868
- if (config.parseErrors.length > 0) return issues;
1869
- for (const server2 of config.servers) {
1870
- if (server2.transport !== "stdio" || !server2.command) continue;
1871
- if (process.platform === "win32" && config.scope === "project" && server2.command === "npx") {
1872
- issues.push({
1873
- severity: "error",
1874
- check: "mcp-commands",
1875
- ruleId: "windows-npx-no-wrapper",
1876
- line: server2.line,
1877
- message: `Server "${server2.name}": npx requires "cmd /c" wrapper on Windows`,
1878
- suggestion: 'Change command to "cmd" and prepend "/c", "npx" to args: ["/c", "npx", ...]',
1879
- fix: buildNpxFix(config, server2.name, server2.args)
1880
- });
1881
- }
1882
- if (LOCAL_PATH_PATTERN.test(server2.command)) {
1883
- const resolved = path7.resolve(projectRoot, server2.command);
1884
- if (!fileExistsSafe(resolved)) {
1885
- issues.push({
1886
- severity: "warning",
1887
- check: "mcp-commands",
1888
- ruleId: "command-not-found",
1889
- line: server2.line,
1890
- message: `Server "${server2.name}": command "${server2.command}" not found`
1891
- });
1892
- }
1893
- }
1894
- if (server2.args) {
1895
- for (const arg of server2.args) {
1896
- if (LOCAL_PATH_PATTERN.test(arg) || FILE_PATH_PATTERN.test(arg)) {
1897
- const resolved = path7.resolve(projectRoot, arg);
1898
- if (!fileExistsSafe(resolved)) {
1899
- issues.push({
1900
- severity: "warning",
1901
- check: "mcp-commands",
1902
- ruleId: "args-path-missing",
1903
- line: server2.line,
1904
- message: `Server "${server2.name}": arg "${arg}" looks like a file path but doesn't exist`
1905
- });
1906
- }
1907
- }
1908
- }
1909
- }
1910
- }
1911
- return issues;
1912
- }
1913
- function fileExistsSafe(filePath) {
1914
- try {
1915
- fs4.accessSync(filePath);
1916
- return true;
1917
- } catch {
1918
- return false;
1919
- }
1920
- }
1921
- function buildNpxFix(config, serverName, _args) {
1922
- const lines = config.content.split("\n");
1923
- for (let i = 0; i < lines.length; i++) {
1924
- const line = lines[i];
1925
- if (line.includes('"command"') && line.includes('"npx"')) {
1926
- let inRightServer = false;
1927
- for (let j = i - 1; j >= 0; j--) {
1928
- if (lines[j].includes(`"${serverName}"`)) {
1929
- inRightServer = true;
1930
- break;
1931
- }
1932
- if (lines[j].includes('"command"')) break;
1933
- }
1934
- if (!inRightServer) continue;
1935
- return {
1936
- file: config.filePath,
1937
- line: i + 1,
1938
- oldText: '"npx"',
1939
- newText: '"cmd"'
1940
- };
1941
- }
1942
- }
1943
- return void 0;
1944
- }
1945
-
1946
- // src/core/checks/mcp/deprecated.ts
1947
- async function checkMcpDeprecated(config, _projectRoot) {
1948
- const issues = [];
1949
- if (config.parseErrors.length > 0) return issues;
1950
- for (const server2 of config.servers) {
1951
- if (server2.transport === "sse") {
1952
- const line = findTypeLine(config.content, server2.name) || server2.line;
1953
- issues.push({
1954
- severity: "warning",
1955
- check: "mcp-deprecated",
1956
- ruleId: "sse-transport",
1957
- line,
1958
- message: `Server "${server2.name}" uses deprecated SSE transport \u2014 use "http" (Streamable HTTP) instead`,
1959
- fix: {
1960
- file: config.filePath,
1961
- line,
1962
- oldText: '"sse"',
1963
- newText: '"http"'
1964
- }
1965
- });
1966
- }
1967
- }
1968
- return issues;
1969
- }
1970
- function findTypeLine(content, serverName) {
1971
- const lines = content.split("\n");
1972
- let inServer = false;
1973
- for (let i = 0; i < lines.length; i++) {
1974
- if (lines[i].includes(`"${serverName}"`)) {
1975
- inServer = true;
1976
- }
1977
- if (inServer && lines[i].includes('"type"') && lines[i].includes('"sse"')) {
1978
- return i + 1;
1979
- }
1980
- if (inServer && i > 0 && lines[i].match(/^\s{4}"\w/) && !lines[i].includes(`"${serverName}"`)) {
1981
- const indent = lines[i].search(/\S/);
1982
- const serverIndent = lines.findIndex((l) => l.includes(`"${serverName}"`));
1983
- if (serverIndent >= 0) {
1984
- const origIndent = lines[serverIndent].search(/\S/);
1985
- if (indent <= origIndent) break;
1986
- }
1987
- }
1988
- }
1989
- return null;
1990
- }
1991
-
1992
- // src/core/checks/mcp/env.ts
1993
- var HAS_CURSOR_SYNTAX = /\$\{env:[^}]+\}/;
1994
- var HAS_CLAUDE_SYNTAX = /\$\{[A-Za-z_][A-Za-z0-9_]*(?::-.*)?\}/;
1995
- var HAS_CONTINUE_SYNTAX = /\$\{\{\s*secrets\.[^}]+\}\}/;
1996
- var ANY_ENV_REF = /\$\{[^}]+\}/g;
1997
- function extractEnvVarRefs(value) {
1998
- const refs = [];
1999
- let match;
2000
- ANY_ENV_REF.lastIndex = 0;
2001
- while ((match = ANY_ENV_REF.exec(value)) !== null) {
2002
- const full = match[0];
2003
- let varName = null;
2004
- const cursorMatch = full.match(/^\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}$/);
2005
- if (cursorMatch) {
2006
- varName = cursorMatch[1];
2007
- }
2008
- const continueMatch = full.match(/^\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}$/);
2009
- if (continueMatch) {
2010
- varName = continueMatch[1];
2011
- }
2012
- const claudeMatch = full.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-.*)?\}$/);
2013
- if (!varName && claudeMatch) {
2014
- varName = claudeMatch[1];
2015
- }
2016
- if (varName) {
2017
- refs.push({ varName, fullMatch: full });
2018
- }
2019
- }
2020
- return refs;
2021
- }
2022
- function collectAllStringValues(server2) {
2023
- const values = [];
2024
- if (server2.command) values.push(server2.command);
2025
- if (server2.args) values.push(...server2.args);
2026
- if (server2.url) values.push(server2.url);
2027
- if (server2.headers) values.push(...Object.values(server2.headers));
2028
- if (server2.env) values.push(...Object.values(server2.env));
2029
- return values;
2030
- }
2031
- async function checkMcpEnv(config, _projectRoot) {
2032
- const issues = [];
2033
- if (config.parseErrors.length > 0) return issues;
2034
- for (const server2 of config.servers) {
2035
- const allValues = collectAllStringValues(server2);
2036
- for (const value of allValues) {
2037
- if (config.client === "claude-code") {
2038
- if (HAS_CURSOR_SYNTAX.test(value)) {
2039
- issues.push({
2040
- severity: "error",
2041
- check: "mcp-env",
2042
- ruleId: "wrong-syntax",
2043
- line: server2.line,
2044
- message: `Server "${server2.name}": Claude Code uses \${VAR}, not \${env:VAR}`,
2045
- fix: buildSyntaxFix(config, server2.line, value, "claude-code")
2046
- });
2047
- }
2048
- }
2049
- if (config.client === "cursor") {
2050
- if (HAS_CLAUDE_SYNTAX.test(value) && !HAS_CURSOR_SYNTAX.test(value) && !HAS_CONTINUE_SYNTAX.test(value)) {
2051
- issues.push({
2052
- severity: "error",
2053
- check: "mcp-env",
2054
- ruleId: "wrong-syntax",
2055
- line: server2.line,
2056
- message: `Server "${server2.name}": Cursor uses \${env:VAR}, not \${VAR}`,
2057
- fix: buildSyntaxFix(config, server2.line, value, "cursor")
2058
- });
2059
- }
2060
- }
2061
- if (config.client === "continue") {
2062
- if ((HAS_CLAUDE_SYNTAX.test(value) || HAS_CURSOR_SYNTAX.test(value)) && !HAS_CONTINUE_SYNTAX.test(value)) {
2063
- issues.push({
2064
- severity: "error",
2065
- check: "mcp-env",
2066
- ruleId: "wrong-syntax",
2067
- line: server2.line,
2068
- message: `Server "${server2.name}": Continue uses \${{ secrets.VAR }}, not \${VAR}`,
2069
- fix: buildSyntaxFix(config, server2.line, value, "continue")
2070
- });
2071
- }
2072
- }
2073
- }
2074
- for (const value of allValues) {
2075
- const refs = extractEnvVarRefs(value);
2076
- for (const ref of refs) {
2077
- if (!(ref.varName in process.env)) {
2078
- issues.push({
2079
- severity: "info",
2080
- check: "mcp-env",
2081
- ruleId: "unset-variable",
2082
- line: server2.line,
2083
- message: `Server "${server2.name}": environment variable "${ref.varName}" is not set`
2084
- });
2085
- }
2086
- }
2087
- }
2088
- if (server2.env && Object.keys(server2.env).length === 0) {
2089
- issues.push({
2090
- severity: "info",
2091
- check: "mcp-env",
2092
- ruleId: "empty-env-block",
2093
- line: server2.line,
2094
- message: `Server "${server2.name}": empty "env" block can be removed`
2095
- });
2096
- }
2097
- }
2098
- return issues;
2099
- }
2100
- function buildSyntaxFix(config, line, value, targetClient) {
2101
- if (targetClient === "claude-code") {
2102
- const fixed = value.replace(/\$\{env:([^}]+)\}/g, "${$1}");
2103
- return { file: config.filePath, line, oldText: value, newText: fixed };
2104
- }
2105
- if (targetClient === "cursor") {
2106
- const fixed = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-.*)?\}/g, (match, varName) => {
2107
- if (match.startsWith("${env:")) return match;
2108
- return `\${env:${varName}}`;
2109
- });
2110
- return { file: config.filePath, line, oldText: value, newText: fixed };
2111
- }
2112
- if (targetClient === "continue") {
2113
- let fixed = value.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "${{ secrets.$1 }}");
2114
- fixed = fixed.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-.*)?\}/g, (match) => {
2115
- if (match.includes("secrets.")) return match;
2116
- const varMatch = match.match(/\$\{([A-Za-z_][A-Za-z0-9_]*)/);
2117
- return varMatch ? `\${{ secrets.${varMatch[1]} }}` : match;
2118
- });
2119
- return { file: config.filePath, line, oldText: value, newText: fixed };
2120
- }
2121
- return void 0;
2122
- }
2123
-
2124
- // src/core/checks/mcp/urls.ts
2125
- var ENV_VAR_REF2 = /\$\{[^}]+\}/;
2126
- async function checkMcpUrls(config, _projectRoot) {
2127
- const issues = [];
2128
- if (config.parseErrors.length > 0) return issues;
2129
- for (const server2 of config.servers) {
2130
- if (!server2.url) continue;
2131
- if (ENV_VAR_REF2.test(server2.url)) continue;
2132
- let parsed;
2133
- try {
2134
- parsed = new URL(server2.url);
2135
- } catch {
2136
- issues.push({
2137
- severity: "error",
2138
- check: "mcp-urls",
2139
- ruleId: "malformed-url",
2140
- line: server2.line,
2141
- message: `Server "${server2.name}": invalid URL "${server2.url}"`
2142
- });
2143
- continue;
2144
- }
2145
- if (config.scope === "project" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1")) {
2146
- issues.push({
2147
- severity: "warning",
2148
- check: "mcp-urls",
2149
- ruleId: "localhost-in-project-config",
2150
- line: server2.line,
2151
- message: `Server "${server2.name}": localhost URL in project config won't work for teammates`
2152
- });
2153
- }
2154
- if (!parsed.pathname || parsed.pathname === "/") {
2155
- issues.push({
2156
- severity: "info",
2157
- check: "mcp-urls",
2158
- ruleId: "missing-path",
2159
- line: server2.line,
2160
- message: `Server "${server2.name}": URL has no path \u2014 most MCP servers expect /mcp`
2161
- });
2162
- }
2163
- }
2164
- return issues;
2165
- }
2166
-
2167
- // src/core/checks/mcp/consistency.ts
2168
- async function checkMcpConsistency(configs) {
2169
- const issues = [];
2170
- if (configs.length < 2) {
2171
- return checkSingleFileIssues(configs).concat(checkMissingFromClient(configs));
2172
- }
2173
- const serverMap = /* @__PURE__ */ new Map();
2174
- for (const config of configs) {
2175
- for (const server2 of config.servers) {
2176
- const existing = serverMap.get(server2.name) || [];
2177
- existing.push({
2178
- config,
2179
- command: server2.command,
2180
- url: server2.url,
2181
- args: server2.args,
2182
- line: server2.line
2183
- });
2184
- serverMap.set(server2.name, existing);
2185
- }
2186
- }
2187
- for (const [name, entries] of serverMap) {
2188
- if (entries.length < 2) continue;
2189
- for (let i = 0; i < entries.length; i++) {
2190
- for (let j = i + 1; j < entries.length; j++) {
2191
- const a = entries[i];
2192
- const b = entries[j];
2193
- const aKey = JSON.stringify({ cmd: a.command, url: a.url, args: a.args });
2194
- const bKey = JSON.stringify({ cmd: b.command, url: b.url, args: b.args });
2195
- if (aKey !== bKey) {
2196
- issues.push({
2197
- severity: "warning",
2198
- check: "mcp-consistency",
2199
- ruleId: "same-server-different-config",
2200
- line: a.line,
2201
- message: `Server "${name}" is configured differently in ${a.config.relativePath} and ${b.config.relativePath}`
2202
- });
2203
- }
2204
- }
2205
- }
2206
- }
2207
- issues.push(...checkMissingFromClient(configs));
2208
- issues.push(...checkSingleFileIssues(configs));
2209
- return issues;
2210
- }
2211
- function checkMissingFromClient(configs) {
2212
- const issues = [];
2213
- const primary = configs.find((c) => c.relativePath === ".mcp.json" && c.scope === "project");
2214
- if (!primary) return issues;
2215
- const otherConfigs = configs.filter(
2216
- (c) => c !== primary && c.scope === "project" && (c.relativePath.includes(".cursor/") || c.relativePath.includes(".vscode/") || c.relativePath.includes(".amazonq/"))
2217
- );
2218
- for (const other of otherConfigs) {
2219
- const otherNames = new Set(other.servers.map((s) => s.name));
2220
- for (const primaryServer of primary.servers) {
2221
- if (!otherNames.has(primaryServer.name)) {
2222
- issues.push({
2223
- severity: "info",
2224
- check: "mcp-consistency",
2225
- ruleId: "missing-from-client",
2226
- line: primaryServer.line,
2227
- message: `Server "${primaryServer.name}" is in .mcp.json but missing from ${other.relativePath}`
2228
- });
2229
- }
2230
- }
2231
- }
2232
- return issues;
2233
- }
2234
- function checkSingleFileIssues(configs) {
2235
- const issues = [];
2236
- for (const config of configs) {
2237
- const nameCount = /* @__PURE__ */ new Map();
2238
- const lines = config.content.split("\n");
2239
- for (const server2 of config.servers) {
2240
- const escaped = server2.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2241
- const pattern = new RegExp(`"${escaped}"\\s*:`, "g");
2242
- let count = 0;
2243
- for (const line of lines) {
2244
- if (pattern.test(line)) count++;
2245
- pattern.lastIndex = 0;
2246
- }
2247
- nameCount.set(server2.name, count);
2248
- }
2249
- for (const [name, count] of nameCount) {
2250
- if (count > 1) {
2251
- issues.push({
2252
- severity: "warning",
2253
- check: "mcp-consistency",
2254
- ruleId: "duplicate-server-name",
2255
- line: 1,
2256
- message: `Duplicate server name "${name}" in ${config.relativePath} \u2014 only the last definition is used`
2257
- });
2258
- }
2259
- }
2260
- }
2261
- return issues;
2262
- }
2263
-
2264
- // src/core/checks/mcp/redundancy.ts
2265
- async function checkMcpRedundancy(configs) {
2266
- const issues = [];
2267
- for (const config of configs) {
2268
- for (const server2 of config.servers) {
2269
- if (server2.disabled === true) {
2270
- issues.push({
2271
- severity: "info",
2272
- check: "mcp-redundancy",
2273
- ruleId: "disabled-server",
2274
- line: server2.line,
2275
- message: `Server "${server2.name}" is disabled \u2014 consider removing it if no longer needed`
2276
- });
2277
- }
2278
- }
2279
- }
2280
- const projectConfigs = configs.filter((c) => c.scope === "project");
2281
- const globalConfigs = configs.filter((c) => c.scope === "user" || c.scope === "global");
2282
- for (const projectConfig of projectConfigs) {
2283
- for (const projectServer of projectConfig.servers) {
2284
- for (const globalConfig of globalConfigs) {
2285
- const globalServer = globalConfig.servers.find((s) => s.name === projectServer.name);
2286
- if (!globalServer) continue;
2287
- const projectKey = JSON.stringify({
2288
- command: projectServer.command,
2289
- args: projectServer.args,
2290
- url: projectServer.url,
2291
- env: projectServer.env
2292
- });
2293
- const globalKey = JSON.stringify({
2294
- command: globalServer.command,
2295
- args: globalServer.args,
2296
- url: globalServer.url,
2297
- env: globalServer.env
2298
- });
2299
- if (projectKey === globalKey) {
2300
- issues.push({
2301
- severity: "info",
2302
- check: "mcp-redundancy",
2303
- ruleId: "identical-across-scopes",
2304
- line: projectServer.line,
2305
- message: `Server "${projectServer.name}" is identically configured in both ${projectConfig.relativePath} and ${globalConfig.relativePath}`
2306
- });
2307
- }
2308
- }
2309
- }
2310
- }
2311
- return issues;
2312
- }
2313
-
2314
- // src/core/session-scanner.ts
2315
- import { readFile as readFile2, readdir, stat } from "fs/promises";
2316
- import { dirname as dirname2, join as join4, resolve as resolve4 } from "path";
2317
- import { existsSync } from "fs";
2318
- import { simpleGit as simpleGit2 } from "simple-git";
2319
-
2320
- // src/core/session-parser.ts
2321
- import { readFile } from "fs/promises";
2322
- var PATH_PATTERN2 = /(?:^|\s|['"`(])([.~/][^\s'"`),;:!?]+)/g;
2323
- function decodeProjectDir(dirName) {
2324
- const parts = dirName.split("--");
2325
- if (parts.length <= 1) return dirName;
2326
- if (parts[0].length === 1 && /^[A-Z]$/i.test(parts[0])) {
2327
- return parts[0] + ":/" + parts.slice(1).join("/");
2328
- }
2329
- return "/" + parts.join("/");
2330
- }
2331
- function extractPaths(content) {
2332
- const paths = [];
2333
- for (const match of content.matchAll(PATH_PATTERN2)) {
2334
- const p = match[1].replace(/[)}\]]+$/, "");
2335
- if (p.length > 2 && !p.startsWith("http") && !p.startsWith("//")) {
2336
- paths.push(p);
2337
- }
2338
- }
2339
- return [...new Set(paths)];
2340
- }
2341
- function parseFrontmatter2(content) {
2342
- const lines = content.split("\n");
2343
- if (lines[0]?.trim() !== "---") {
2344
- return { body: content };
2345
- }
2346
- const endIdx = lines.indexOf("---", 1);
2347
- if (endIdx === -1) {
2348
- return { body: content };
2349
- }
2350
- const frontmatter = {};
2351
- for (let i = 1; i < endIdx; i++) {
2352
- const line = lines[i];
2353
- const colonIdx = line.indexOf(":");
2354
- if (colonIdx > 0) {
2355
- const key = line.slice(0, colonIdx).trim();
2356
- const value = line.slice(colonIdx + 1).trim();
2357
- frontmatter[key] = value;
2358
- }
2359
- }
2360
- return {
2361
- name: frontmatter["name"],
2362
- description: frontmatter["description"],
2363
- type: frontmatter["type"],
2364
- body: lines.slice(endIdx + 1).join("\n")
2365
- };
2366
- }
2367
- async function parseMemoryFile(filePath, projectDir) {
2368
- const content = await readFile(filePath, "utf-8");
2369
- const { name, description, type, body } = parseFrontmatter2(content);
2370
- const referencedPaths = extractPaths(body);
2371
- return {
2372
- filePath,
2373
- projectDir,
2374
- name,
2375
- description,
2376
- type,
2377
- content: body,
2378
- referencedPaths
2379
- };
2380
- }
2381
-
2382
- // src/core/session-scanner.ts
2383
- var home = process.env.HOME || process.env.USERPROFILE || "";
2384
- var AGENT_DIRS = [
2385
- { provider: "claude-code", dir: join4(home, ".claude"), historyFile: "history.jsonl" },
2386
- { provider: "codex-cli", dir: join4(home, ".codex"), historyFile: "history.jsonl" },
2387
- { provider: "vibe-cli", dir: join4(home, ".vibe") },
2388
- {
2389
- provider: "amazon-q",
2390
- dir: join4(home, ".aws", "amazonq")
2391
- },
2392
- {
2393
- provider: "goose",
2394
- dir: process.platform === "win32" ? join4(process.env.APPDATA || "", "Block", "goose") : join4(home, ".config", "goose")
2395
- },
2396
- { provider: "continue", dir: join4(home, ".continue") },
2397
- {
2398
- provider: "windsurf",
2399
- dir: join4(home, ".windsurf")
2400
- }
2401
- ];
2402
- function detectProviders() {
2403
- return AGENT_DIRS.filter((a) => existsSync(a.dir)).map((a) => a.provider);
2404
- }
2405
- async function parseJsonlFiltered(filePath, filter) {
2406
- if (!existsSync(filePath)) return [];
2407
- const content = await readFile2(filePath, "utf-8");
2408
- const results = [];
2409
- for (const line of content.split("\n")) {
2410
- if (!line.trim()) continue;
2411
- try {
2412
- const parsed = JSON.parse(line);
2413
- const result = filter(parsed);
2414
- if (result) results.push(result);
2415
- } catch {
2416
- }
2417
- }
2418
- return results;
2419
- }
2420
- async function readClaudeHistory() {
2421
- const historyPath = join4(home, ".claude", "history.jsonl");
2422
- return parseJsonlFiltered(historyPath, (entry) => {
2423
- if (!entry.display || !entry.project) return null;
2424
- return {
2425
- display: entry.display,
2426
- timestamp: entry.timestamp || 0,
2427
- project: entry.project.replace(/\\/g, "/"),
2428
- sessionId: entry.sessionId || "",
2429
- provider: "claude-code"
2430
- };
2431
- });
2432
- }
2433
- async function readCodexHistory() {
2434
- const historyPath = join4(home, ".codex", "history.jsonl");
2435
- return parseJsonlFiltered(historyPath, (entry) => {
2436
- if (!entry.display && !entry.command) return null;
2437
- return {
2438
- display: entry.display || entry.command || "",
2439
- timestamp: entry.timestamp || 0,
2440
- project: (entry.project || entry.cwd || "").replace(/\\/g, "/"),
2441
- sessionId: entry.sessionId || "",
2442
- provider: "codex-cli"
2443
- };
2444
- });
2445
- }
2446
- async function readClaudeMemories() {
2447
- const projectsDir = join4(home, ".claude", "projects");
2448
- if (!existsSync(projectsDir)) return [];
2449
- const memories = [];
2450
- const projectDirs = await readdir(projectsDir).catch(() => []);
2451
- for (const projDir of projectDirs) {
2452
- const memoryDir = join4(projectsDir, projDir, "memory");
2453
- if (!existsSync(memoryDir)) continue;
2454
- const decodedPath = decodeProjectDir(projDir);
2455
- const files = await readdir(memoryDir).catch(() => []);
2456
- for (const file of files) {
2457
- if (!file.endsWith(".md") || file === "MEMORY.md") continue;
2458
- try {
2459
- const entry = await parseMemoryFile(join4(memoryDir, file), decodedPath);
2460
- memories.push(entry);
2461
- } catch {
2462
- }
2463
- }
2464
- }
2465
- return memories;
2466
- }
2467
- function detectAiderInSiblings(siblings) {
2468
- for (const sib of siblings) {
2469
- if (existsSync(join4(sib.path, ".aider.chat.history.md"))) {
2470
- return "aider";
2471
- }
2472
- }
2473
- return null;
2474
- }
2475
- async function detectSiblings(projectRoot) {
2476
- const parentDir = dirname2(resolve4(projectRoot));
2477
- const siblings = [];
2478
- let entries;
2479
- try {
2480
- entries = await readdir(parentDir);
2481
- } catch {
2482
- return [];
2483
- }
2484
- for (const entry of entries) {
2485
- const fullPath = join4(parentDir, entry);
2486
- const entryPath = resolve4(fullPath);
2487
- if (entryPath === resolve4(projectRoot)) continue;
2488
- try {
2489
- const s = await stat(fullPath);
2490
- if (!s.isDirectory()) continue;
2491
- } catch {
2492
- continue;
2493
- }
2494
- if (entry.startsWith(".") || entry === "node_modules") continue;
2495
- const isProject = existsSync(join4(fullPath, ".git")) || existsSync(join4(fullPath, "package.json")) || existsSync(join4(fullPath, "Cargo.toml")) || existsSync(join4(fullPath, "go.mod")) || existsSync(join4(fullPath, "pyproject.toml"));
2496
- if (!isProject) continue;
2497
- const sibling = {
2498
- path: entryPath.replace(/\\/g, "/"),
2499
- name: entry
2500
- };
2501
- try {
2502
- const git = simpleGit2(fullPath);
2503
- const remotes = await git.getRemotes(true);
2504
- const origin = remotes.find((r) => r.name === "origin");
2505
- if (origin?.refs?.fetch) {
2506
- sibling.gitRemoteUrl = origin.refs.fetch;
2507
- const orgMatch = origin.refs.fetch.match(/github\.com[:/]([^/]+)\//);
2508
- if (orgMatch) sibling.gitOrg = orgMatch[1];
2509
- }
2510
- } catch {
2511
- }
2512
- siblings.push(sibling);
2513
- }
2514
- if (siblings.length > 50) {
2515
- const currentGit = simpleGit2(projectRoot);
2516
- let currentOrg;
2517
- try {
2518
- const remotes = await currentGit.getRemotes(true);
2519
- const origin = remotes.find((r) => r.name === "origin");
2520
- const orgMatch = origin?.refs?.fetch?.match(/github\.com[:/]([^/]+)\//);
2521
- if (orgMatch) currentOrg = orgMatch[1];
2522
- } catch {
2523
- }
2524
- if (currentOrg) {
2525
- return siblings.filter((s) => s.gitOrg === currentOrg);
2526
- }
2527
- }
2528
- return siblings;
2529
- }
2530
- async function scanSessionData(projectRoot) {
2531
- const providers = detectProviders();
2532
- const historyPromises = [];
2533
- if (providers.includes("claude-code")) historyPromises.push(readClaudeHistory());
2534
- if (providers.includes("codex-cli")) historyPromises.push(readCodexHistory());
2535
- const [histories, memories, siblings] = await Promise.all([
2536
- Promise.all(historyPromises).then((arrays) => arrays.flat()),
2537
- providers.includes("claude-code") ? readClaudeMemories() : Promise.resolve([]),
2538
- detectSiblings(projectRoot)
2539
- ]);
2540
- const aider = detectAiderInSiblings(siblings);
2541
- if (aider && !providers.includes("aider")) {
2542
- providers.push("aider");
2543
- }
2544
- return {
2545
- history: histories,
2546
- memories,
2547
- siblings,
2548
- currentProject: resolve4(projectRoot).replace(/\\/g, "/"),
2549
- providers
2550
- };
2551
- }
2552
-
2553
- // src/core/checks/session/missing-secret.ts
2554
- import { resolve as resolve5 } from "path";
2555
- var SECRET_SET_PATTERN = /gh\s+secret\s+set\s+(\S+)\s+(?:--repo\s+(\S+)|.*-b\s+)/;
2556
- var SECRET_SET_SIMPLE = /gh\s+secret\s+set\s+(\S+)/;
2557
- async function checkMissingSecret(ctx) {
2558
- const issues = [];
2559
- const secrets = [];
2560
- for (const entry of ctx.history) {
2561
- const match = entry.display.match(SECRET_SET_PATTERN) || entry.display.match(SECRET_SET_SIMPLE);
2562
- if (!match) continue;
2563
- secrets.push({
2564
- name: match[1],
2565
- repo: match[2],
2566
- project: entry.project
2567
- });
2568
- }
2569
- if (secrets.length === 0) return issues;
2570
- const byName = /* @__PURE__ */ new Map();
2571
- for (const s of secrets) {
2572
- if (!byName.has(s.name)) byName.set(s.name, /* @__PURE__ */ new Set());
2573
- byName.get(s.name).add(s.project);
2574
- if (s.repo) byName.get(s.name).add(s.repo);
2575
- }
2576
- const currentNorm = resolve5(ctx.currentProject).replace(/\\/g, "/");
2577
- for (const [secretName, projects] of byName) {
2578
- const currentHas = [...projects].some(
2579
- (p) => p.includes(currentNorm) || currentNorm.includes(p.replace(/\\/g, "/")) || p.includes(resolve5(ctx.currentProject).split(/[/\\]/).pop() || "")
2580
- );
2581
- if (currentHas) continue;
2582
- const siblingMatches = ctx.siblings.filter(
2583
- (sib) => [...projects].some(
2584
- (p) => p.replace(/\\/g, "/").includes(sib.name) || p.includes(sib.path)
2585
- )
2586
- );
2587
- if (siblingMatches.length >= 2) {
2588
- const sibNames = siblingMatches.map((s) => s.name).join(", ");
2589
- issues.push({
2590
- severity: "error",
2591
- check: "session-missing-secret",
2592
- ruleId: "session/missing-secret",
2593
- line: 0,
2594
- message: `GitHub secret "${secretName}" is set on ${siblingMatches.length} sibling repos (${sibNames}) but not on this project`,
2595
- suggestion: `Run: gh secret set ${secretName} --repo <owner>/<repo>`,
2596
- detail: `Found in agent history: ${siblingMatches.length} sibling repos have this secret configured`
2597
- });
2598
- }
2599
- }
2600
- return issues;
2601
- }
2602
-
2603
- // src/core/checks/session/diverged-file.ts
2604
- import { readFile as readFile3 } from "fs/promises";
2605
- import { join as join5 } from "path";
2606
- import { existsSync as existsSync2 } from "fs";
2607
- var CANONICAL_FILES = [
2608
- "release.sh",
2609
- ".github/workflows/ci.yml",
2610
- ".github/workflows/release.yml",
2611
- "biome.json",
2612
- ".prettierrc",
2613
- ".eslintrc.json",
2614
- "tsconfig.json",
2615
- ".gitignore"
2616
- ];
2617
- function calculateOverlap(a, b) {
2618
- const linesA = a.split("\n").map((l) => l.trim()).filter((l) => l.length > 3);
2619
- const linesB = new Set(
2620
- b.split("\n").map((l) => l.trim()).filter((l) => l.length > 3)
2621
- );
2622
- if (linesA.length === 0 && linesB.size === 0) return 1;
2623
- if (linesA.length === 0 || linesB.size === 0) return 0;
2624
- let matches = 0;
2625
- for (const line of linesA) {
2626
- if (linesB.has(line)) matches++;
2627
- }
2628
- return matches / Math.max(linesA.length, linesB.size);
2629
- }
2630
- async function checkDivergedFile(ctx) {
2631
- const issues = [];
2632
- for (const fileName of CANONICAL_FILES) {
2633
- const currentPath = join5(ctx.currentProject, fileName);
2634
- if (!existsSync2(currentPath)) continue;
2635
- let currentContent;
2636
- try {
2637
- currentContent = await readFile3(currentPath, "utf-8");
2638
- } catch {
2639
- continue;
2640
- }
2641
- const diverged = [];
2642
- for (const sib of ctx.siblings) {
2643
- const sibPath = join5(sib.path, fileName);
2644
- if (!existsSync2(sibPath)) continue;
2645
- try {
2646
- const sibContent = await readFile3(sibPath, "utf-8");
2647
- const overlap = calculateOverlap(currentContent, sibContent);
2648
- if (overlap >= 0.2 && overlap < 0.9) {
2649
- diverged.push({ sibling: sib.name, overlap: Math.round(overlap * 100) });
2650
- }
2651
- } catch {
2652
- continue;
2653
- }
2654
- }
2655
- if (diverged.length > 0) {
2656
- const details = diverged.map((d) => `${d.sibling} (${d.overlap}% overlap)`).join(", ");
2657
- issues.push({
2658
- severity: "warning",
2659
- check: "session-diverged-file",
2660
- ruleId: "session/diverged-file",
2661
- line: 0,
2662
- message: `${fileName} has diverged from sibling repos: ${details}`,
2663
- suggestion: `Compare with sibling versions to identify unintentional drift`,
2664
- detail: `Files with the same name across sibling repos should be kept in sync when they serve the same purpose`
2665
- });
2666
- }
2667
- }
2668
- return issues;
2669
- }
2670
-
2671
- // src/core/checks/session/missing-workflow.ts
2672
- import { readdir as readdir2 } from "fs/promises";
2673
- import { join as join6 } from "path";
2674
- import { existsSync as existsSync3 } from "fs";
2675
- async function checkMissingWorkflow(ctx) {
2676
- const issues = [];
2677
- const currentWorkflowDir = join6(ctx.currentProject, ".github", "workflows");
2678
- if (!existsSync3(join6(ctx.currentProject, ".github"))) return issues;
2679
- const currentWorkflows = /* @__PURE__ */ new Set();
2680
- if (existsSync3(currentWorkflowDir)) {
2681
- try {
2682
- const files = await readdir2(currentWorkflowDir);
2683
- for (const f of files) {
2684
- if (f.endsWith(".yml") || f.endsWith(".yaml")) currentWorkflows.add(f);
2685
- }
2686
- } catch {
2687
- }
2688
- }
2689
- const workflowMap = /* @__PURE__ */ new Map();
2690
- for (const sib of ctx.siblings) {
2691
- const sibWorkflowDir = join6(sib.path, ".github", "workflows");
2692
- if (!existsSync3(sibWorkflowDir)) continue;
2693
- try {
2694
- const files = await readdir2(sibWorkflowDir);
2695
- for (const f of files) {
2696
- if (!(f.endsWith(".yml") || f.endsWith(".yaml"))) continue;
2697
- if (!workflowMap.has(f)) workflowMap.set(f, []);
2698
- workflowMap.get(f).push(sib.name);
2699
- }
2700
- } catch {
2701
- continue;
2702
- }
2703
- }
2704
- for (const [workflow, siblings] of workflowMap) {
2705
- if (currentWorkflows.has(workflow)) continue;
2706
- if (siblings.length < 2) continue;
2707
- const sibNames = siblings.join(", ");
2708
- issues.push({
2709
- severity: "warning",
2710
- check: "session-missing-workflow",
2711
- ruleId: "session/missing-workflow",
2712
- line: 0,
2713
- message: `GitHub Actions workflow "${workflow}" exists in ${siblings.length} sibling repos (${sibNames}) but not in this project`,
2714
- suggestion: `Consider adding .github/workflows/${workflow} for consistency`,
2715
- detail: `Sibling repos with this workflow: ${sibNames}`
2716
- });
2717
- }
2718
- return issues;
2719
- }
2720
-
2721
- // src/core/checks/session/stale-memory.ts
2722
- import { existsSync as existsSync4 } from "fs";
2723
- import { resolve as resolve6, isAbsolute } from "path";
2724
- async function checkStaleMemory(ctx) {
2725
- const issues = [];
2726
- const currentNorm = ctx.currentProject.replace(/\\/g, "/");
2727
- const projectMemories = ctx.memories.filter(
2728
- (m) => m.projectDir.replace(/\\/g, "/") === currentNorm
2729
- );
2730
- for (const mem of projectMemories) {
2731
- const brokenPaths = [];
2732
- for (const ref of mem.referencedPaths) {
2733
- const fullPath = isAbsolute(ref) ? ref : resolve6(ctx.currentProject, ref);
2734
- if (!existsSync4(fullPath)) {
2735
- brokenPaths.push(ref);
2736
- }
2737
- }
2738
- if (brokenPaths.length > 0) {
2739
- const name = mem.name || mem.filePath.split(/[/\\]/).pop() || "unknown";
2740
- issues.push({
2741
- severity: "info",
2742
- check: "session-stale-memory",
2743
- ruleId: "session/stale-memory",
2744
- line: 0,
2745
- message: `Memory "${name}" references ${brokenPaths.length} path(s) that no longer exist: ${brokenPaths.join(", ")}`,
2746
- suggestion: `Update or remove the memory file: ${mem.filePath}`,
2747
- detail: `Memory files with broken path references may cause the AI agent to follow stale instructions`
2748
- });
2749
- }
2750
- }
2751
- return issues;
2752
- }
2753
-
2754
- // src/core/checks/session/duplicate-memory.ts
2755
- function calculateLineOverlap2(a, b) {
2756
- const linesA = a.split("\n").map((l) => l.trim()).filter((l) => l.length > 5);
2757
- const linesB = new Set(
2758
- b.split("\n").map((l) => l.trim()).filter((l) => l.length > 5)
2759
- );
2760
- if (linesA.length === 0 || linesB.size === 0) return 0;
2761
- let matches = 0;
2762
- for (const line of linesA) {
2763
- if (linesB.has(line)) matches++;
2764
- }
2765
- return matches / Math.max(linesA.length, linesB.size);
2766
- }
2767
- async function checkDuplicateMemory(ctx) {
2768
- const issues = [];
2769
- const reported = /* @__PURE__ */ new Set();
2770
- for (let i = 0; i < ctx.memories.length; i++) {
2771
- for (let j = i + 1; j < ctx.memories.length; j++) {
2772
- const a = ctx.memories[i];
2773
- const b = ctx.memories[j];
2774
- if (a.projectDir.replace(/\\/g, "/") === b.projectDir.replace(/\\/g, "/")) continue;
2775
- if (a.content.length < 50 || b.content.length < 50) continue;
2776
- const overlap = calculateLineOverlap2(a.content, b.content);
2777
- if (overlap < 0.6) continue;
2778
- const pairKey = [a.filePath, b.filePath].sort().join("::");
2779
- if (reported.has(pairKey)) continue;
2780
- reported.add(pairKey);
2781
- const nameA = a.name || a.filePath.split(/[/\\]/).pop() || "unknown";
2782
- const nameB = b.name || b.filePath.split(/[/\\]/).pop() || "unknown";
2783
- const projA = a.projectDir.split(/[/\\]/).pop() || a.projectDir;
2784
- const projB = b.projectDir.split(/[/\\]/).pop() || b.projectDir;
2785
- issues.push({
2786
- severity: "info",
2787
- check: "session-duplicate-memory",
2788
- ruleId: "session/duplicate-memory",
2789
- line: 0,
2790
- message: `Memory "${nameA}" (${projA}) and "${nameB}" (${projB}) have ${Math.round(overlap * 100)}% overlap`,
2791
- suggestion: `Consider consolidating into a shared memory or removing the duplicate`,
2792
- detail: `Near-duplicate memories across projects waste context and may drift out of sync`
2793
- });
2794
- }
2795
- }
2796
- return issues;
2797
- }
2798
-
2799
- // src/version.ts
2800
- function loadVersion() {
2801
- if (true) return "0.7.0";
2802
- const fs6 = __require("fs");
2803
- const path9 = __require("path");
2804
- const pkgPath = path9.resolve(__dirname, "../package.json");
2805
- const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
2806
- return pkg.version;
2807
- }
2808
- var VERSION = loadVersion();
2809
-
2810
- // src/core/audit.ts
2811
- var ALL_CHECKS = [
2812
- "paths",
2813
- "commands",
2814
- "staleness",
2815
- "tokens",
2816
- "redundancy",
2817
- "contradictions",
2818
- "frontmatter"
2819
- ];
2820
- var ALL_MCP_CHECKS = [
2821
- "mcp-schema",
2822
- "mcp-security",
2823
- "mcp-commands",
2824
- "mcp-deprecated",
2825
- "mcp-env",
2826
- "mcp-urls",
2827
- "mcp-consistency",
2828
- "mcp-redundancy"
2829
- ];
2830
- var ALL_SESSION_CHECKS = [
2831
- "session-missing-secret",
2832
- "session-diverged-file",
2833
- "session-missing-workflow",
2834
- "session-stale-memory",
2835
- "session-duplicate-memory"
2836
- ];
2837
- function hasMcpChecks(checks) {
2838
- return checks.some((c) => c.startsWith("mcp-"));
2839
- }
2840
- function hasSessionChecks(checks) {
2841
- return checks.some((c) => c.startsWith("session-"));
2842
- }
2843
- async function runAudit(projectRoot, activeChecks, options = {}) {
2844
- const fileResults = [];
2845
- const shouldRunContextChecks = !options.mcpOnly && !options.sessionOnly;
2846
- const shouldRunMcpChecks = options.mcp || options.mcpGlobal || options.mcpOnly || hasMcpChecks(activeChecks);
2847
- const shouldRunSessionChecks = options.session || options.sessionOnly || hasSessionChecks(activeChecks);
2848
- if (shouldRunContextChecks) {
2849
- const discovered = await scanForContextFiles(projectRoot, {
2850
- depth: options.depth,
2851
- extraPatterns: options.extraPatterns
2852
- });
2853
- const parsed = discovered.map((f) => parseContextFile(f));
2854
- for (const file of parsed) {
2855
- const checkPromises = [];
2856
- if (activeChecks.includes("paths")) checkPromises.push(checkPaths(file, projectRoot));
2857
- if (activeChecks.includes("commands")) checkPromises.push(checkCommands(file, projectRoot));
2858
- if (activeChecks.includes("staleness")) checkPromises.push(checkStaleness(file, projectRoot));
2859
- if (activeChecks.includes("tokens")) checkPromises.push(checkTokens(file, projectRoot));
2860
- if (activeChecks.includes("redundancy"))
2861
- checkPromises.push(checkRedundancy(file, projectRoot));
2862
- if (activeChecks.includes("frontmatter"))
2863
- checkPromises.push(checkFrontmatter(file, projectRoot));
2864
- const results = await Promise.all(checkPromises);
2865
- const issues = results.flat();
2866
- fileResults.push({
2867
- path: file.relativePath,
2868
- isSymlink: file.isSymlink,
2869
- symlinkTarget: file.symlinkTarget,
2870
- tokens: file.totalTokens,
2871
- lines: file.totalLines,
2872
- issues
2873
- });
2874
- }
2875
- if (activeChecks.includes("tokens")) {
2876
- const aggIssue = checkAggregateTokens(
2877
- fileResults.map((f) => ({ path: f.path, tokens: f.tokens }))
2878
- );
2879
- if (aggIssue && fileResults.length > 0) fileResults[0].issues.push(aggIssue);
2880
- }
2881
- if (activeChecks.includes("redundancy")) {
2882
- const dupIssues = checkDuplicateContent(parsed);
2883
- if (dupIssues.length > 0 && fileResults.length > 0) fileResults[0].issues.push(...dupIssues);
2884
- }
2885
- if (activeChecks.includes("contradictions")) {
2886
- const contradictionIssues = checkContradictions(parsed);
2887
- if (contradictionIssues.length > 0 && fileResults.length > 0)
2888
- fileResults[0].issues.push(...contradictionIssues);
2889
- }
2890
- }
2891
- if (shouldRunMcpChecks) {
2892
- const mcpFiles = await scanForMcpConfigs(projectRoot);
2893
- const mcpConfigs = mcpFiles.map(
2894
- (f) => parseMcpConfig(f, projectRoot, "project")
2895
- );
2896
- if (options.mcpGlobal) {
2897
- const globalFiles = await scanGlobalMcpConfigs();
2898
- mcpConfigs.push(...globalFiles.map((f) => parseMcpConfig(f, projectRoot, "user")));
2899
- }
2900
- const activeMcpChecks = activeChecks.filter((c) => c.startsWith("mcp-"));
2901
- const mcpChecksToRun = activeMcpChecks.length > 0 ? activeMcpChecks : options.mcp || options.mcpGlobal || options.mcpOnly ? ALL_MCP_CHECKS : [];
2902
- for (const config of mcpConfigs) {
2903
- const checkPromises = [];
2904
- if (mcpChecksToRun.includes("mcp-schema"))
2905
- checkPromises.push(checkMcpSchema(config, projectRoot));
2906
- if (mcpChecksToRun.includes("mcp-security"))
2907
- checkPromises.push(checkMcpSecurity(config, projectRoot));
2908
- if (mcpChecksToRun.includes("mcp-commands"))
2909
- checkPromises.push(checkMcpCommands(config, projectRoot));
2910
- if (mcpChecksToRun.includes("mcp-deprecated"))
2911
- checkPromises.push(checkMcpDeprecated(config, projectRoot));
2912
- if (mcpChecksToRun.includes("mcp-env")) checkPromises.push(checkMcpEnv(config, projectRoot));
2913
- if (mcpChecksToRun.includes("mcp-urls"))
2914
- checkPromises.push(checkMcpUrls(config, projectRoot));
2915
- const results = await Promise.all(checkPromises);
2916
- const issues = results.flat();
2917
- const lines = config.content.split("\n").length;
2918
- fileResults.push({
2919
- path: config.relativePath,
2920
- isSymlink: false,
2921
- tokens: 0,
2922
- lines,
2923
- issues
2924
- });
2925
- }
2926
- if (mcpChecksToRun.includes("mcp-consistency")) {
2927
- const consistencyIssues = await checkMcpConsistency(mcpConfigs);
2928
- if (consistencyIssues.length > 0) {
2929
- const firstMcpResult = fileResults.find(
2930
- (f) => mcpConfigs.some((c) => c.relativePath === f.path)
2931
- );
2932
- if (firstMcpResult) {
2933
- firstMcpResult.issues.push(...consistencyIssues);
2934
- }
2935
- }
2936
- }
2937
- if (mcpChecksToRun.includes("mcp-redundancy")) {
2938
- const redundancyIssues = await checkMcpRedundancy(mcpConfigs);
2939
- if (redundancyIssues.length > 0) {
2940
- const firstMcpResult = fileResults.find(
2941
- (f) => mcpConfigs.some((c) => c.relativePath === f.path)
2942
- );
2943
- if (firstMcpResult) {
2944
- firstMcpResult.issues.push(...redundancyIssues);
2945
- }
2946
- }
2947
- }
2948
- }
2949
- if (shouldRunSessionChecks) {
2950
- const activeSessionChecks = activeChecks.filter(
2951
- (c) => c.startsWith("session-")
2952
- );
2953
- const sessionChecksToRun = activeSessionChecks.length > 0 ? activeSessionChecks : options.session || options.sessionOnly ? ALL_SESSION_CHECKS : [];
2954
- if (sessionChecksToRun.length > 0) {
2955
- const sessionCtx = await scanSessionData(projectRoot);
2956
- const sessionPromises = [];
2957
- if (sessionChecksToRun.includes("session-missing-secret"))
2958
- sessionPromises.push(checkMissingSecret(sessionCtx));
2959
- if (sessionChecksToRun.includes("session-diverged-file"))
2960
- sessionPromises.push(checkDivergedFile(sessionCtx));
2961
- if (sessionChecksToRun.includes("session-missing-workflow"))
2962
- sessionPromises.push(checkMissingWorkflow(sessionCtx));
2963
- if (sessionChecksToRun.includes("session-stale-memory"))
2964
- sessionPromises.push(checkStaleMemory(sessionCtx));
2965
- if (sessionChecksToRun.includes("session-duplicate-memory"))
2966
- sessionPromises.push(checkDuplicateMemory(sessionCtx));
2967
- const sessionResults = await Promise.all(sessionPromises);
2968
- const sessionIssues = sessionResults.flat();
2969
- if (sessionIssues.length > 0) {
2970
- fileResults.push({
2971
- path: "~/.claude/ (session audit)",
2972
- isSymlink: false,
2973
- tokens: 0,
2974
- lines: 0,
2975
- issues: sessionIssues
2976
- });
2977
- }
2978
- }
2979
- }
2980
- let estimatedWaste = 0;
2981
- for (const fr of fileResults) {
2982
- for (const issue of fr.issues) {
2983
- if (issue.check === "redundancy" && issue.suggestion) {
2984
- const tokenMatch = issue.suggestion.match(/~(\d+)\s+tokens/);
2985
- if (tokenMatch) estimatedWaste += parseInt(tokenMatch[1], 10);
2986
- }
2987
- }
2988
- }
2989
- return {
2990
- version: VERSION,
2991
- scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
2992
- projectRoot,
2993
- files: fileResults,
2994
- summary: {
2995
- errors: fileResults.reduce(
2996
- (sum, f) => sum + f.issues.filter((i) => i.severity === "error").length,
2997
- 0
2998
- ),
2999
- warnings: fileResults.reduce(
3000
- (sum, f) => sum + f.issues.filter((i) => i.severity === "warning").length,
3001
- 0
3002
- ),
3003
- info: fileResults.reduce(
3004
- (sum, f) => sum + f.issues.filter((i) => i.severity === "info").length,
3005
- 0
3006
- ),
3007
- totalTokens: fileResults.reduce((sum, f) => sum + f.tokens, 0),
3008
- estimatedWaste
3009
- }
3010
- };
3011
- }
3012
-
3013
- // src/core/fixer.ts
3014
- import * as fs5 from "fs";
3015
- import chalk from "chalk";
3016
- function applyFixes(result) {
3017
- const fixesByFile = /* @__PURE__ */ new Map();
3018
- for (const file of result.files) {
3019
- for (const issue of file.issues) {
3020
- if (issue.fix) {
3021
- const existing = fixesByFile.get(issue.fix.file) || [];
3022
- existing.push(issue.fix);
3023
- fixesByFile.set(issue.fix.file, existing);
3024
- }
3025
- }
3026
- }
3027
- let totalFixes = 0;
3028
- const filesModified = [];
3029
- for (const [filePath, fixes] of fixesByFile) {
3030
- const content = fs5.readFileSync(filePath, "utf-8");
3031
- const lines = content.split("\n");
3032
- let modified = false;
3033
- const sortedFixes = [...fixes].sort((a, b) => b.line - a.line);
3034
- for (const fix of sortedFixes) {
3035
- const lineIdx = fix.line - 1;
3036
- if (lineIdx < 0 || lineIdx >= lines.length) continue;
3037
- const line = lines[lineIdx];
3038
- if (line.includes(fix.oldText)) {
3039
- lines[lineIdx] = line.replace(fix.oldText, fix.newText);
3040
- modified = true;
3041
- totalFixes++;
3042
- console.log(
3043
- chalk.green(" Fixed") + ` Line ${fix.line}: ${chalk.dim(fix.oldText)} ${chalk.dim("\u2192")} ${fix.newText}`
3044
- );
3045
- }
3046
- }
3047
- if (modified) {
3048
- const newContent = lines.join("\n");
3049
- if (filePath.endsWith(".json")) {
3050
- try {
3051
- JSON.parse(newContent);
3052
- } catch {
3053
- console.log(chalk.yellow(" Skipped") + ` ${filePath}: fix would produce invalid JSON`);
3054
- continue;
3055
- }
3056
- }
3057
- fs5.writeFileSync(filePath, newContent, "utf-8");
3058
- filesModified.push(filePath);
3059
- }
3060
- }
3061
- return { totalFixes, filesModified };
3062
- }
3063
-
3064
- // src/mcp/server.ts
3065
- import * as path8 from "path";
3066
- var checkEnum = z.enum([
3067
- "paths",
3068
- "commands",
3069
- "staleness",
3070
- "tokens",
3071
- "redundancy",
3072
- "contradictions",
3073
- "frontmatter",
3074
- "mcp-schema",
3075
- "mcp-security",
3076
- "mcp-commands",
3077
- "mcp-deprecated",
3078
- "mcp-env",
3079
- "mcp-urls",
3080
- "mcp-consistency",
3081
- "mcp-redundancy",
3082
- "session-missing-secret",
3083
- "session-diverged-file",
3084
- "session-missing-workflow",
3085
- "session-stale-memory",
3086
- "session-duplicate-memory"
3087
- ]);
3088
- var server = new McpServer({
3089
- name: "ctxlint",
3090
- version: VERSION
3091
- });
3092
- server.tool(
3093
- "ctxlint_audit",
3094
- "Audit all AI agent context files (CLAUDE.md, AGENTS.md, etc.) and optionally MCP server configs in the project. Checks for stale references, invalid commands, redundant content, contradictions, frontmatter issues, token waste, and MCP config errors.",
3095
- {
3096
- projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
3097
- checks: z.array(checkEnum).optional().describe("Which checks to run. Defaults to all.")
3098
- },
3099
- {
3100
- readOnlyHint: true,
3101
- destructiveHint: false,
3102
- idempotentHint: true,
3103
- openWorldHint: false
3104
- },
3105
- async ({ projectPath, checks }) => {
3106
- const root = path8.resolve(projectPath || process.cwd());
3107
- const activeChecks = checks || ALL_CHECKS;
3108
- try {
3109
- const result = await runAudit(root, activeChecks);
3110
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
3111
- } catch (err) {
3112
- const msg = err instanceof Error ? err.message : String(err);
3113
- return {
3114
- content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
3115
- isError: true
3116
- };
3117
- } finally {
3118
- freeEncoder();
3119
- resetGit();
3120
- }
3121
- }
3122
- );
3123
- server.tool(
3124
- "ctxlint_validate_path",
3125
- "Check if a file path referenced in a context file actually exists in the project. Returns the file status and suggests corrections if the path is invalid.",
3126
- {
3127
- path: z.string().describe("The file path to validate"),
3128
- projectPath: z.string().optional().describe("Project root. Defaults to cwd.")
3129
- },
3130
- {
3131
- readOnlyHint: true,
3132
- destructiveHint: false,
3133
- idempotentHint: true,
3134
- openWorldHint: false
3135
- },
3136
- async ({ path: filePath, projectPath }) => {
3137
- try {
3138
- const root = path8.resolve(projectPath || process.cwd());
3139
- const resolved = path8.resolve(root, filePath);
3140
- const result = {
3141
- path: filePath,
3142
- exists: fileExists(resolved) || isDirectory(resolved)
3143
- };
3144
- if (!result.exists) {
3145
- const rename = await findRenames(root, filePath);
3146
- if (rename) {
3147
- result.renamed = true;
3148
- result.newPath = rename.newPath;
3149
- result.renameCommit = rename.commitHash;
3150
- result.daysAgo = rename.daysAgo;
3151
- }
3152
- }
3153
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
3154
- } catch (err) {
3155
- const msg = err instanceof Error ? err.message : String(err);
3156
- return {
3157
- content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
3158
- isError: true
3159
- };
3160
- } finally {
3161
- resetGit();
3162
- }
3163
- }
3164
- );
3165
- server.tool(
3166
- "ctxlint_token_report",
3167
- "Get a token count breakdown for all context files in the project. Shows per-file and aggregate token usage, plus estimated waste from redundant content.",
3168
- {
3169
- projectPath: z.string().optional().describe("Project root. Defaults to cwd.")
3170
- },
3171
- {
3172
- readOnlyHint: true,
3173
- destructiveHint: false,
3174
- idempotentHint: true,
3175
- openWorldHint: false
3176
- },
3177
- async ({ projectPath }) => {
3178
- const root = path8.resolve(projectPath || process.cwd());
3179
- try {
3180
- const discovered = await scanForContextFiles(root);
3181
- const parsed = discovered.map((f) => parseContextFile(f));
3182
- const files = parsed.map((f) => ({
3183
- path: f.relativePath,
3184
- tokens: f.totalTokens,
3185
- lines: f.totalLines,
3186
- isSymlink: f.isSymlink
3187
- }));
3188
- const totalTokens = files.reduce((sum, f) => sum + f.tokens, 0);
3189
- return {
3190
- content: [
3191
- {
3192
- type: "text",
3193
- text: JSON.stringify(
3194
- { files, totalTokens, note: "Token counts use GPT-4 cl100k_base tokenizer" },
3195
- null,
3196
- 2
3197
- )
3198
- }
3199
- ]
3200
- };
3201
- } catch (err) {
3202
- const msg = err instanceof Error ? err.message : String(err);
3203
- return {
3204
- content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
3205
- isError: true
3206
- };
3207
- } finally {
3208
- freeEncoder();
3209
- }
3210
- }
3211
- );
3212
- server.tool(
3213
- "ctxlint_fix",
3214
- "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.",
3215
- {
3216
- projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
3217
- checks: z.array(checkEnum).optional().describe("Which checks to run before fixing. Defaults to all.")
3218
- },
3219
- {
3220
- readOnlyHint: false,
3221
- destructiveHint: false,
3222
- idempotentHint: true,
3223
- openWorldHint: false
3224
- },
3225
- async ({ projectPath, checks }) => {
3226
- const root = path8.resolve(projectPath || process.cwd());
3227
- const activeChecks = checks || ALL_CHECKS;
3228
- try {
3229
- const result = await runAudit(root, activeChecks);
3230
- const fixSummary = applyFixes(result);
3231
- return {
3232
- content: [
3233
- {
3234
- type: "text",
3235
- text: JSON.stringify(
3236
- {
3237
- totalFixes: fixSummary.totalFixes,
3238
- filesModified: fixSummary.filesModified,
3239
- remainingIssues: result.summary
3240
- },
3241
- null,
3242
- 2
3243
- )
3244
- }
3245
- ]
3246
- };
3247
- } catch (err) {
3248
- const msg = err instanceof Error ? err.message : String(err);
3249
- return {
3250
- content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
3251
- isError: true
3252
- };
3253
- } finally {
3254
- freeEncoder();
3255
- resetGit();
3256
- }
3257
- }
3258
- );
3259
- server.tool(
3260
- "ctxlint_mcp_audit",
3261
- "Lint MCP server configuration files in a project. Checks for schema errors, hardcoded secrets, deprecated transports, wrong env var syntax, URL issues, and cross-client inconsistencies.",
3262
- {
3263
- projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
3264
- checks: z.array(checkEnum).optional().describe("Specific MCP checks to run (default: all mcp-* checks)."),
3265
- includeGlobal: z.boolean().optional().describe("Also scan global/user-level MCP configs.")
3266
- },
3267
- {
3268
- readOnlyHint: true,
3269
- destructiveHint: false,
3270
- idempotentHint: true,
3271
- openWorldHint: false
3272
- },
3273
- async ({ projectPath, checks, includeGlobal }) => {
3274
- const root = path8.resolve(projectPath || process.cwd());
3275
- const activeChecks = checks || ALL_MCP_CHECKS;
3276
- try {
3277
- const result = await runAudit(root, activeChecks, {
3278
- mcp: true,
3279
- mcpOnly: true,
3280
- mcpGlobal: includeGlobal || false
3281
- });
3282
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
3283
- } catch (err) {
3284
- const msg = err instanceof Error ? err.message : String(err);
3285
- return {
3286
- content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
3287
- isError: true
3288
- };
3289
- } finally {
3290
- freeEncoder();
3291
- resetGit();
3292
- }
3293
- }
3294
- );
3295
- server.tool(
3296
- "ctxlint_session_audit",
3297
- "Audit AI agent session data for cross-project consistency. Checks for missing GitHub secrets, diverged config files, missing workflows, stale memory entries, and duplicate memories across sibling repositories.",
3298
- {
3299
- projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
3300
- checks: z.array(checkEnum).optional().describe("Specific session checks to run (default: all session-* checks).")
3301
- },
3302
- {
3303
- readOnlyHint: true,
3304
- destructiveHint: false,
3305
- idempotentHint: true,
3306
- openWorldHint: true
3307
- },
3308
- async ({ projectPath, checks }) => {
3309
- const root = path8.resolve(projectPath || process.cwd());
3310
- const activeChecks = checks || ALL_SESSION_CHECKS;
3311
- try {
3312
- const result = await runAudit(root, activeChecks, {
3313
- session: true,
3314
- sessionOnly: true
3315
- });
3316
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
3317
- } catch (err) {
3318
- const msg = err instanceof Error ? err.message : String(err);
3319
- return {
3320
- content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
3321
- isError: true
3322
- };
3323
- } finally {
3324
- freeEncoder();
3325
- resetGit();
3326
- }
3327
- }
3328
- );
3329
- var transport = new StdioServerTransport();
3330
- await server.connect(transport);