@yawlabs/ctxlint 0.9.18 → 0.9.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21934,11 +21934,11 @@ var init_protocol = __esm({
21934
21934
  *
21935
21935
  * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
21936
21936
  */
21937
- async connect(transport2) {
21937
+ async connect(transport) {
21938
21938
  if (this._transport) {
21939
21939
  throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
21940
21940
  }
21941
- this._transport = transport2;
21941
+ this._transport = transport;
21942
21942
  const _onclose = this.transport?.onclose;
21943
21943
  this._transport.onclose = () => {
21944
21944
  _onclose?.();
@@ -26254,49 +26254,49 @@ var require_fast_uri = __commonJS({
26254
26254
  schemelessOptions.skipEscape = true;
26255
26255
  return serialize(resolved, schemelessOptions);
26256
26256
  }
26257
- function resolveComponent(base, relative4, options, skipNormalization) {
26257
+ function resolveComponent(base, relative5, options, skipNormalization) {
26258
26258
  const target = {};
26259
26259
  if (!skipNormalization) {
26260
26260
  base = parse4(serialize(base, options), options);
26261
- relative4 = parse4(serialize(relative4, options), options);
26261
+ relative5 = parse4(serialize(relative5, options), options);
26262
26262
  }
26263
26263
  options = options || {};
26264
- if (!options.tolerant && relative4.scheme) {
26265
- target.scheme = relative4.scheme;
26266
- target.userinfo = relative4.userinfo;
26267
- target.host = relative4.host;
26268
- target.port = relative4.port;
26269
- target.path = removeDotSegments(relative4.path || "");
26270
- target.query = relative4.query;
26264
+ if (!options.tolerant && relative5.scheme) {
26265
+ target.scheme = relative5.scheme;
26266
+ target.userinfo = relative5.userinfo;
26267
+ target.host = relative5.host;
26268
+ target.port = relative5.port;
26269
+ target.path = removeDotSegments(relative5.path || "");
26270
+ target.query = relative5.query;
26271
26271
  } else {
26272
- if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
26273
- target.userinfo = relative4.userinfo;
26274
- target.host = relative4.host;
26275
- target.port = relative4.port;
26276
- target.path = removeDotSegments(relative4.path || "");
26277
- target.query = relative4.query;
26272
+ if (relative5.userinfo !== void 0 || relative5.host !== void 0 || relative5.port !== void 0) {
26273
+ target.userinfo = relative5.userinfo;
26274
+ target.host = relative5.host;
26275
+ target.port = relative5.port;
26276
+ target.path = removeDotSegments(relative5.path || "");
26277
+ target.query = relative5.query;
26278
26278
  } else {
26279
- if (!relative4.path) {
26279
+ if (!relative5.path) {
26280
26280
  target.path = base.path;
26281
- if (relative4.query !== void 0) {
26282
- target.query = relative4.query;
26281
+ if (relative5.query !== void 0) {
26282
+ target.query = relative5.query;
26283
26283
  } else {
26284
26284
  target.query = base.query;
26285
26285
  }
26286
26286
  } else {
26287
- if (relative4.path[0] === "/") {
26288
- target.path = removeDotSegments(relative4.path);
26287
+ if (relative5.path[0] === "/") {
26288
+ target.path = removeDotSegments(relative5.path);
26289
26289
  } else {
26290
26290
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
26291
- target.path = "/" + relative4.path;
26291
+ target.path = "/" + relative5.path;
26292
26292
  } else if (!base.path) {
26293
- target.path = relative4.path;
26293
+ target.path = relative5.path;
26294
26294
  } else {
26295
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
26295
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative5.path;
26296
26296
  }
26297
26297
  target.path = removeDotSegments(target.path);
26298
26298
  }
26299
- target.query = relative4.query;
26299
+ target.query = relative5.query;
26300
26300
  }
26301
26301
  target.userinfo = base.userinfo;
26302
26302
  target.host = base.host;
@@ -26304,7 +26304,7 @@ var require_fast_uri = __commonJS({
26304
26304
  }
26305
26305
  target.scheme = base.scheme;
26306
26306
  }
26307
- target.fragment = relative4.fragment;
26307
+ target.fragment = relative5.fragment;
26308
26308
  return target;
26309
26309
  }
26310
26310
  function equal(uriA, uriB, options) {
@@ -30411,8 +30411,8 @@ var init_mcp = __esm({
30411
30411
  *
30412
30412
  * The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
30413
30413
  */
30414
- async connect(transport2) {
30415
- return await this.server.connect(transport2);
30414
+ async connect(transport) {
30415
+ return await this.server.connect(transport);
30416
30416
  }
30417
30417
  /**
30418
30418
  * Closes the connection.
@@ -34201,10 +34201,13 @@ globstar while`, t2, d, e, f, m), this.matchOne(t2.slice(d), e.slice(f), s)) ret
34201
34201
  // src/utils/fs.ts
34202
34202
  import * as fs2 from "node:fs";
34203
34203
  import * as path from "node:path";
34204
+ function stripBom(content) {
34205
+ return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
34206
+ }
34204
34207
  function loadPackageJson(projectRoot) {
34205
34208
  if (pkgJsonCache?.root === projectRoot) return pkgJsonCache.data;
34206
34209
  try {
34207
- const content = fs2.readFileSync(path.join(projectRoot, "package.json"), "utf-8");
34210
+ const content = stripBom(fs2.readFileSync(path.join(projectRoot, "package.json"), "utf-8"));
34208
34211
  const data = JSON.parse(content);
34209
34212
  pkgJsonCache = { root: projectRoot, data };
34210
34213
  return data;
@@ -34247,7 +34250,7 @@ function readSymlinkTarget(filePath) {
34247
34250
  }
34248
34251
  function readFileContent(filePath) {
34249
34252
  try {
34250
- return fs2.readFileSync(filePath, "utf-8");
34253
+ return stripBom(fs2.readFileSync(filePath, "utf-8"));
34251
34254
  } catch (err) {
34252
34255
  const code = err.code;
34253
34256
  if (code === "ENOENT") {
@@ -40656,10 +40659,9 @@ var init_git = __esm({
40656
40659
  });
40657
40660
 
40658
40661
  // src/core/mcp-parser.ts
40659
- async function parseMcpConfig(file2, projectRoot, scopeOverride) {
40662
+ async function parseMcpConfig(file2, projectRoot, scope) {
40660
40663
  const content = readFileContent(file2.absolutePath);
40661
40664
  const client = detectClient(file2.relativePath);
40662
- const scope = scopeOverride ?? detectScope(file2.relativePath);
40663
40665
  const expectedRootKey = client === "vscode" ? "servers" : "mcpServers";
40664
40666
  const isGitTracked = await checkGitTracked(file2.absolutePath, projectRoot);
40665
40667
  const result = {
@@ -40703,10 +40705,10 @@ async function parseMcpConfig(file2, projectRoot, scopeOverride) {
40703
40705
  }
40704
40706
  const raw = value;
40705
40707
  const line = findServerLine(lines, name);
40706
- const transport2 = inferTransport(raw);
40708
+ const transport = inferTransport(raw);
40707
40709
  const entry = {
40708
40710
  name,
40709
- transport: transport2,
40711
+ transport,
40710
40712
  line,
40711
40713
  raw
40712
40714
  };
@@ -40718,7 +40720,7 @@ async function parseMcpConfig(file2, projectRoot, scopeOverride) {
40718
40720
  if (typeof raw.disabled === "boolean") entry.disabled = raw.disabled;
40719
40721
  if (Array.isArray(raw.autoApprove)) entry.autoApprove = raw.autoApprove.map(String);
40720
40722
  if (typeof raw.timeout === "number") entry.timeout = raw.timeout;
40721
- if (typeof raw.oauth === "object" && raw.oauth !== null)
40723
+ if (typeof raw.oauth === "object" && raw.oauth !== null && !Array.isArray(raw.oauth))
40722
40724
  entry.oauth = raw.oauth;
40723
40725
  if (typeof raw.headersHelper === "string") entry.headersHelper = raw.headersHelper;
40724
40726
  result.servers.push(entry);
@@ -40737,9 +40739,6 @@ function detectClient(relativePath) {
40737
40739
  }
40738
40740
  return "claude-code";
40739
40741
  }
40740
- function detectScope(_relativePath) {
40741
- return "project";
40742
- }
40743
40742
  function findRootKey(parsed) {
40744
40743
  if ("mcpServers" in parsed) return "mcpServers";
40745
40744
  if ("servers" in parsed) return "servers";
@@ -41316,6 +41315,28 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
41316
41315
  }
41317
41316
  return result;
41318
41317
  }
41318
+ function getNodeValue(node) {
41319
+ switch (node.type) {
41320
+ case "array":
41321
+ return node.children.map(getNodeValue);
41322
+ case "object":
41323
+ const obj = /* @__PURE__ */ Object.create(null);
41324
+ for (let prop of node.children) {
41325
+ const valueNode = prop.children[1];
41326
+ if (valueNode) {
41327
+ obj[prop.children[0].value] = getNodeValue(valueNode);
41328
+ }
41329
+ }
41330
+ return obj;
41331
+ case "null":
41332
+ case "string":
41333
+ case "number":
41334
+ case "boolean":
41335
+ return node.value;
41336
+ default:
41337
+ return void 0;
41338
+ }
41339
+ }
41319
41340
  function visit(text, visitor, options = ParseOptions.DEFAULT) {
41320
41341
  const _scanner = createScanner(text, false);
41321
41342
  const _jsonPath = [];
@@ -41664,7 +41685,7 @@ var init_edit = __esm({
41664
41685
  });
41665
41686
 
41666
41687
  // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js
41667
- var ScanError, SyntaxKind, parseTree2, ParseErrorCode;
41688
+ var ScanError, SyntaxKind, parseTree2, getNodeValue2, ParseErrorCode;
41668
41689
  var init_main = __esm({
41669
41690
  "node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js"() {
41670
41691
  "use strict";
@@ -41701,6 +41722,7 @@ var init_main = __esm({
41701
41722
  SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
41702
41723
  })(SyntaxKind || (SyntaxKind = {}));
41703
41724
  parseTree2 = parseTree;
41725
+ getNodeValue2 = getNodeValue;
41704
41726
  (function(ParseErrorCode2) {
41705
41727
  ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
41706
41728
  ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
@@ -41723,9 +41745,9 @@ var init_main = __esm({
41723
41745
  });
41724
41746
 
41725
41747
  // src/core/mcph-parser.ts
41726
- async function parseMchpConfig(file2, projectRoot, scopeOverride) {
41748
+ async function parseMcphConfig(file2, projectRoot, scopeOverride) {
41727
41749
  const content = readFileContent(file2.absolutePath);
41728
- const scope = scopeOverride ?? detectScope2(file2.relativePath);
41750
+ const scope = scopeOverride ?? detectScope(file2.relativePath);
41729
41751
  const isGitTracked = await checkGitTracked2(file2.absolutePath, projectRoot);
41730
41752
  const isGitignored = await checkGitignored(file2.absolutePath, projectRoot);
41731
41753
  const result = {
@@ -41754,11 +41776,7 @@ async function parseMchpConfig(file2, projectRoot, scopeOverride) {
41754
41776
  result.parseErrors.push(".mcph.json must be a JSON object at the root");
41755
41777
  return result;
41756
41778
  }
41757
- try {
41758
- result.raw = JSON.parse(stripComments2(content));
41759
- } catch {
41760
- return result;
41761
- }
41779
+ result.raw = getNodeValue2(tree);
41762
41780
  const rootProps = tree.children ?? [];
41763
41781
  for (const prop of rootProps) {
41764
41782
  if (prop.type !== "property" || !prop.children || prop.children.length < 2) continue;
@@ -41787,7 +41805,7 @@ async function parseMchpConfig(file2, projectRoot, scopeOverride) {
41787
41805
  }
41788
41806
  return result;
41789
41807
  }
41790
- function detectScope2(relativePath) {
41808
+ function detectScope(relativePath) {
41791
41809
  const normalized = relativePath.replace(/\\/g, "/");
41792
41810
  if (normalized.startsWith("~/")) return "global";
41793
41811
  if (normalized.endsWith(".mcph.local.json")) return "project-local";
@@ -41816,46 +41834,6 @@ function offsetToPosition(content, offset) {
41816
41834
  }
41817
41835
  return { line, column };
41818
41836
  }
41819
- function stripComments2(src) {
41820
- let out = "";
41821
- let i2 = 0;
41822
- let inString = false;
41823
- let escape2 = false;
41824
- while (i2 < src.length) {
41825
- const ch = src[i2];
41826
- if (inString) {
41827
- out += ch;
41828
- if (escape2) {
41829
- escape2 = false;
41830
- } else if (ch === "\\") {
41831
- escape2 = true;
41832
- } else if (ch === '"') {
41833
- inString = false;
41834
- }
41835
- i2++;
41836
- continue;
41837
- }
41838
- if (ch === '"') {
41839
- inString = true;
41840
- out += ch;
41841
- i2++;
41842
- continue;
41843
- }
41844
- if (ch === "/" && src[i2 + 1] === "/") {
41845
- while (i2 < src.length && src[i2] !== "\n") i2++;
41846
- continue;
41847
- }
41848
- if (ch === "/" && src[i2 + 1] === "*") {
41849
- i2 += 2;
41850
- while (i2 < src.length && !(src[i2] === "*" && src[i2 + 1] === "/")) i2++;
41851
- i2 += 2;
41852
- continue;
41853
- }
41854
- out += ch;
41855
- i2++;
41856
- }
41857
- return out;
41858
- }
41859
41837
  async function checkGitTracked2(filePath, projectRoot) {
41860
41838
  try {
41861
41839
  const git = getGit(projectRoot);
@@ -42189,29 +42167,33 @@ async function checkPaths(file2, projectRoot) {
42189
42167
  function findClosestMatch(target, files) {
42190
42168
  const targetNorm = target.replace(/\\/g, "/");
42191
42169
  const targetBase = path3.basename(targetNorm);
42192
- let bestMatch = null;
42193
- let bestDistance = Infinity;
42170
+ let basenameMatch = null;
42171
+ let basenameDistance = Infinity;
42194
42172
  for (const file2 of files) {
42195
42173
  const fileNorm = file2.replace(/\\/g, "/");
42196
42174
  if (path3.basename(fileNorm) === targetBase && fileNorm !== targetNorm) {
42197
42175
  const dist = levenshtein(targetNorm, fileNorm);
42198
- if (dist < bestDistance) {
42199
- bestDistance = dist;
42200
- bestMatch = fileNorm;
42176
+ if (dist < basenameDistance) {
42177
+ basenameDistance = dist;
42178
+ basenameMatch = fileNorm;
42201
42179
  }
42202
42180
  }
42203
42181
  }
42204
- if (!bestMatch) {
42205
- for (const file2 of files) {
42206
- const fileNorm = file2.replace(/\\/g, "/");
42207
- const dist = levenshtein(targetNorm, fileNorm);
42208
- if (dist < bestDistance && dist <= Math.max(targetNorm.length * 0.4, 5)) {
42209
- bestDistance = dist;
42210
- bestMatch = fileNorm;
42211
- }
42182
+ if (basenameMatch) return basenameMatch;
42183
+ const absoluteCap = Math.max(targetNorm.length * 0.4, 5);
42184
+ let fullPathMatch = null;
42185
+ let fullPathDistance = Infinity;
42186
+ for (const file2 of files) {
42187
+ const fileNorm = file2.replace(/\\/g, "/");
42188
+ const lenDelta = Math.abs(targetNorm.length - fileNorm.length);
42189
+ if (lenDelta >= fullPathDistance || lenDelta > absoluteCap) continue;
42190
+ const dist = levenshtein(targetNorm, fileNorm);
42191
+ if (dist < fullPathDistance && dist <= absoluteCap) {
42192
+ fullPathDistance = dist;
42193
+ fullPathMatch = fileNorm;
42212
42194
  }
42213
42195
  }
42214
- return bestMatch;
42196
+ return fullPathMatch;
42215
42197
  }
42216
42198
  var import_fast_levenshtein, levenshtein, cachedProjectFiles;
42217
42199
  var init_paths = __esm({
@@ -42229,6 +42211,24 @@ var init_paths = __esm({
42229
42211
  // src/core/checks/commands.ts
42230
42212
  import * as fs4 from "node:fs";
42231
42213
  import * as path4 from "node:path";
42214
+ function extractNpxPackage(cmd) {
42215
+ if (!/^npx\b/.test(cmd)) return null;
42216
+ const tokens = cmd.split(/\s+/).slice(1);
42217
+ for (let i2 = 0; i2 < tokens.length; i2++) {
42218
+ const t2 = tokens[i2];
42219
+ if (t2 === "-p" || t2 === "--package") {
42220
+ const v2 = tokens[i2 + 1];
42221
+ if (v2 && !v2.startsWith("-")) return v2;
42222
+ continue;
42223
+ }
42224
+ if (t2.startsWith("-p=") || t2.startsWith("--package=")) {
42225
+ return t2.slice(t2.indexOf("=") + 1) || null;
42226
+ }
42227
+ if (t2.startsWith("-")) continue;
42228
+ return t2;
42229
+ }
42230
+ return null;
42231
+ }
42232
42232
  async function checkCommands(file2, projectRoot) {
42233
42233
  const issues = [];
42234
42234
  const pkgJson = loadPackageJson(projectRoot);
@@ -42267,10 +42267,9 @@ async function checkCommands(file2, projectRoot) {
42267
42267
  }
42268
42268
  continue;
42269
42269
  }
42270
- const npxMatch = cmd.match(NPX_PATTERN);
42271
- if (npxMatch && pkgJson) {
42272
- const pkgName = npxMatch[1];
42273
- if (pkgName.startsWith("-")) continue;
42270
+ if (/^npx\b/.test(cmd) && pkgJson) {
42271
+ const pkgName = extractNpxPackage(cmd);
42272
+ if (!pkgName) continue;
42274
42273
  const allDeps = {
42275
42274
  ...pkgJson.dependencies,
42276
42275
  ...pkgJson.devDependencies,
@@ -42345,7 +42344,7 @@ async function checkCommands(file2, projectRoot) {
42345
42344
  }
42346
42345
  function loadMakefile(projectRoot) {
42347
42346
  try {
42348
- return fs4.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8");
42347
+ return stripBom(fs4.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8"));
42349
42348
  } catch {
42350
42349
  return null;
42351
42350
  }
@@ -42354,14 +42353,13 @@ function hasMakeTarget(makefile, target) {
42354
42353
  const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
42355
42354
  return pattern.test(makefile);
42356
42355
  }
42357
- var NPM_SCRIPT_PATTERN, MAKE_PATTERN, NPX_PATTERN;
42356
+ var NPM_SCRIPT_PATTERN, MAKE_PATTERN;
42358
42357
  var init_commands = __esm({
42359
42358
  "src/core/checks/commands.ts"() {
42360
42359
  "use strict";
42361
42360
  init_fs();
42362
42361
  NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
42363
42362
  MAKE_PATTERN = /^make\s+(\S+)/;
42364
- NPX_PATTERN = /^npx\s+(\S+)/;
42365
42363
  }
42366
42364
  });
42367
42365
 
@@ -42536,7 +42534,14 @@ function isAlwaysLoaded(file2) {
42536
42534
  return !hasPathsFrontmatter(file2.content);
42537
42535
  }
42538
42536
  const basename4 = rel.split("/").pop() ?? "";
42539
- return ALWAYS_LOADED_BASENAMES.has(basename4);
42537
+ for (const name of ALWAYS_LOADED_NAMES) {
42538
+ if (name.includes("/")) {
42539
+ if (rel === name || rel.endsWith("/" + name)) return true;
42540
+ } else if (basename4 === name) {
42541
+ return true;
42542
+ }
42543
+ }
42544
+ return false;
42540
42545
  }
42541
42546
  function computeSectionCosts(file2) {
42542
42547
  if (file2.sections.length === 0) return [];
@@ -42558,7 +42563,7 @@ function loadSettingsSources(projectRoot) {
42558
42563
  for (const p2 of candidates) {
42559
42564
  let content;
42560
42565
  try {
42561
- content = fs5.readFileSync(p2, "utf-8");
42566
+ content = stripBom(fs5.readFileSync(p2, "utf-8"));
42562
42567
  } catch {
42563
42568
  continue;
42564
42569
  }
@@ -42574,9 +42579,17 @@ function canonicalizeCommand(backticked) {
42574
42579
  const beforeFlags = backticked.trim().split(/\s+--?/, 1)[0];
42575
42580
  return beforeFlags.replace(/\s+/g, " ");
42576
42581
  }
42582
+ function buildCommandPattern(cmd) {
42583
+ const tokens = cmd.split(/\s+/).filter(Boolean);
42584
+ const escaped = tokens.map((t2) => t2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
42585
+ if (tokens.length === 1) {
42586
+ return new RegExp(`(?<![A-Za-z0-9_\\-])${escaped[0]}(?![A-Za-z0-9_\\-])`, "i");
42587
+ }
42588
+ const body = escaped.join("[\\s\\-_]+");
42589
+ return new RegExp(`(?<![A-Za-z0-9])${body}(?![A-Za-z0-9])`, "i");
42590
+ }
42577
42591
  function commandIsEnforced(cmd, settings) {
42578
- const escaped = cmd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42579
- const pattern = new RegExp(`\\b${escaped}\\b`, "i");
42592
+ const pattern = buildCommandPattern(cmd);
42580
42593
  for (const s of settings) {
42581
42594
  for (const entry of s.permissions?.deny ?? []) {
42582
42595
  if (pattern.test(entry)) return true;
@@ -42654,13 +42667,14 @@ function checkAggregateTierTokens(files) {
42654
42667
  suggestion: "Consider moving the largest files or their heaviest sections to on-demand tiers (skills, subagents, memory)."
42655
42668
  };
42656
42669
  }
42657
- var ALWAYS_LOADED_BASENAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
42670
+ var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
42658
42671
  var init_tier_tokens = __esm({
42659
42672
  "src/core/checks/tier-tokens.ts"() {
42660
42673
  "use strict";
42661
42674
  init_tokens();
42675
+ init_fs();
42662
42676
  init_tokens2();
42663
- ALWAYS_LOADED_BASENAMES = /* @__PURE__ */ new Set([
42677
+ ALWAYS_LOADED_NAMES = [
42664
42678
  "CLAUDE.md",
42665
42679
  "CLAUDE.local.md",
42666
42680
  "AGENTS.md",
@@ -42675,15 +42689,42 @@ var init_tier_tokens = __esm({
42675
42689
  ".rules",
42676
42690
  ".goosehints",
42677
42691
  "replit.md",
42678
- "copilot-instructions.md",
42679
- "guidelines.md",
42680
- "instructions.md"
42681
- ]);
42692
+ ".github/copilot-instructions.md",
42693
+ ".junie/guidelines.md",
42694
+ ".junie/AGENTS.md",
42695
+ ".goose/instructions.md"
42696
+ ];
42682
42697
  TOP_SECTIONS_TO_REPORT = 3;
42683
42698
  INVIOLABLE_WITH_COMMAND = /\b(NEVER|ALWAYS|DON'?T|DO NOT|MUST NOT)\b[^.!?`]{0,80}`([^`]+)`/i;
42684
42699
  }
42685
42700
  });
42686
42701
 
42702
+ // src/utils/similarity.ts
42703
+ function jaccardSimilarityFromSets(a, b2, opts = {}) {
42704
+ const bothEmptyIsIdentical = opts.bothEmptyIsIdentical ?? false;
42705
+ if (a.size === 0 && b2.size === 0) {
42706
+ return bothEmptyIsIdentical ? 1 : 0;
42707
+ }
42708
+ if (a.size === 0 || b2.size === 0) return 0;
42709
+ const [small, large] = a.size <= b2.size ? [a, b2] : [b2, a];
42710
+ let intersection2 = 0;
42711
+ for (const line of small) {
42712
+ if (large.has(line)) intersection2++;
42713
+ }
42714
+ const unionSize = a.size + b2.size - intersection2;
42715
+ return intersection2 / unionSize;
42716
+ }
42717
+ function toLineSet(text, minTokenLen) {
42718
+ return new Set(
42719
+ text.split("\n").map((l) => l.trim()).filter((l) => l.length > minTokenLen)
42720
+ );
42721
+ }
42722
+ var init_similarity = __esm({
42723
+ "src/utils/similarity.ts"() {
42724
+ "use strict";
42725
+ }
42726
+ });
42727
+
42687
42728
  // src/core/checks/redundancy.ts
42688
42729
  import * as path7 from "node:path";
42689
42730
  function compilePatterns(allDeps) {
@@ -42706,6 +42747,20 @@ function compilePatterns(allDeps) {
42706
42747
  }
42707
42748
  return compiled;
42708
42749
  }
42750
+ function getCompiledPatterns(projectRoot, allDeps) {
42751
+ const relevant = [];
42752
+ for (const pkg of Object.keys(PACKAGE_TECH_MAP)) {
42753
+ if (allDeps.has(pkg)) relevant.push(pkg);
42754
+ }
42755
+ relevant.sort();
42756
+ const key = `${projectRoot}\0${relevant.join(" ")}`;
42757
+ let compiled = compiledPatternsCache.get(key);
42758
+ if (!compiled) {
42759
+ compiled = compilePatterns(allDeps);
42760
+ compiledPatternsCache.set(key, compiled);
42761
+ }
42762
+ return compiled;
42763
+ }
42709
42764
  async function checkRedundancy(file2, projectRoot) {
42710
42765
  const issues = [];
42711
42766
  const pkgJson = loadPackageJson(projectRoot);
@@ -42716,7 +42771,7 @@ async function checkRedundancy(file2, projectRoot) {
42716
42771
  ...Object.keys(pkgJson.peerDependencies || {}),
42717
42772
  ...Object.keys(pkgJson.optionalDependencies || {})
42718
42773
  ]);
42719
- const compiledPatterns = compilePatterns(allDeps);
42774
+ const compiledPatterns = getCompiledPatterns(projectRoot, allDeps);
42720
42775
  const lines2 = file2.content.split("\n");
42721
42776
  for (let i2 = 0; i2 < lines2.length; i2++) {
42722
42777
  const line = lines2[i2];
@@ -42767,10 +42822,14 @@ async function checkRedundancy(file2, projectRoot) {
42767
42822
  }
42768
42823
  function checkDuplicateContent(files) {
42769
42824
  const issues = [];
42770
- const DUPLICATE_CONTENT_THRESHOLD = 0.6;
42825
+ const lineSets = files.map((f) => toLineSet(f.content, DUPLICATE_CONTENT_MIN_TOKEN_LEN));
42771
42826
  for (let i2 = 0; i2 < files.length; i2++) {
42827
+ const a = lineSets[i2];
42828
+ if (a.size === 0) continue;
42772
42829
  for (let j3 = i2 + 1; j3 < files.length; j3++) {
42773
- const overlap = calculateLineOverlap(files[i2].content, files[j3].content);
42830
+ const b2 = lineSets[j3];
42831
+ if (b2.size === 0) continue;
42832
+ const overlap = jaccardSimilarityFromSets(a, b2);
42774
42833
  if (overlap >= DUPLICATE_CONTENT_THRESHOLD) {
42775
42834
  issues.push({
42776
42835
  severity: "warning",
@@ -42785,29 +42844,15 @@ function checkDuplicateContent(files) {
42785
42844
  }
42786
42845
  return issues;
42787
42846
  }
42788
- function calculateLineOverlap(contentA, contentB) {
42789
- const linesA = new Set(
42790
- contentA.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
42791
- );
42792
- const linesB = new Set(
42793
- contentB.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
42794
- );
42795
- if (linesA.size === 0 || linesB.size === 0) return 0;
42796
- let intersection2 = 0;
42797
- for (const line of linesA) {
42798
- if (linesB.has(line)) intersection2++;
42799
- }
42800
- const unionSize = linesA.size + linesB.size - intersection2;
42801
- return intersection2 / unionSize;
42802
- }
42803
42847
  function escapeRegex2(str) {
42804
42848
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42805
42849
  }
42806
- var PACKAGE_TECH_MAP;
42850
+ var PACKAGE_TECH_MAP, compiledPatternsCache, DUPLICATE_CONTENT_THRESHOLD, DUPLICATE_CONTENT_MIN_TOKEN_LEN;
42807
42851
  var init_redundancy = __esm({
42808
42852
  "src/core/checks/redundancy.ts"() {
42809
42853
  "use strict";
42810
42854
  init_fs();
42855
+ init_similarity();
42811
42856
  init_tokens();
42812
42857
  PACKAGE_TECH_MAP = {
42813
42858
  react: ["React", "react"],
@@ -42860,6 +42905,9 @@ var init_redundancy = __esm({
42860
42905
  cypress: ["Cypress"],
42861
42906
  puppeteer: ["Puppeteer"]
42862
42907
  };
42908
+ compiledPatternsCache = /* @__PURE__ */ new Map();
42909
+ DUPLICATE_CONTENT_THRESHOLD = 0.6;
42910
+ DUPLICATE_CONTENT_MIN_TOKEN_LEN = 10;
42863
42911
  }
42864
42912
  });
42865
42913
 
@@ -43177,7 +43225,7 @@ var init_contradictions = __esm({
43177
43225
  function parseFrontmatter(content) {
43178
43226
  const lines = content.split("\n");
43179
43227
  if (lines[0]?.trim() !== "---") {
43180
- return { found: false, fields: {}, endLine: 0 };
43228
+ return { found: false, fields: {}, endLine: 0, unclosed: false };
43181
43229
  }
43182
43230
  const fields = {};
43183
43231
  let endLine = 0;
@@ -43204,9 +43252,9 @@ function parseFrontmatter(content) {
43204
43252
  }
43205
43253
  }
43206
43254
  if (endLine === 0) {
43207
- return { found: true, fields, endLine: lines.length };
43255
+ return { found: true, fields, endLine: lines.length, unclosed: true };
43208
43256
  }
43209
- return { found: true, fields, endLine };
43257
+ return { found: true, fields, endLine, unclosed: false };
43210
43258
  }
43211
43259
  function isCursorMdc(file2) {
43212
43260
  return file2.relativePath.endsWith(".mdc");
@@ -43217,6 +43265,23 @@ function isCopilotInstructions(file2) {
43217
43265
  function isWindsurfRule(file2) {
43218
43266
  return file2.relativePath.includes(".windsurf/rules/") && file2.relativePath.endsWith(".md");
43219
43267
  }
43268
+ function hasUnbalancedBracketsOrQuotes(val) {
43269
+ let square = 0;
43270
+ let curly = 0;
43271
+ for (const ch of val) {
43272
+ if (ch === "[") square++;
43273
+ else if (ch === "]") square--;
43274
+ else if (ch === "{") curly++;
43275
+ else if (ch === "}") curly--;
43276
+ if (square < 0 || curly < 0) return true;
43277
+ }
43278
+ if (square !== 0 || curly !== 0) return true;
43279
+ const doubleQuotes = (val.match(/"/g) || []).length;
43280
+ const singleQuotes = (val.match(/'/g) || []).length;
43281
+ if (doubleQuotes % 2 !== 0) return true;
43282
+ if (singleQuotes % 2 !== 0) return true;
43283
+ return false;
43284
+ }
43220
43285
  async function checkFrontmatter(file2, _projectRoot) {
43221
43286
  const issues = [];
43222
43287
  if (isCursorMdc(file2)) {
@@ -43228,9 +43293,23 @@ async function checkFrontmatter(file2, _projectRoot) {
43228
43293
  }
43229
43294
  return issues;
43230
43295
  }
43296
+ function unclosedFrontmatterIssue() {
43297
+ return {
43298
+ severity: "error",
43299
+ check: "frontmatter",
43300
+ ruleId: "frontmatter/unclosed",
43301
+ line: 1,
43302
+ message: "Frontmatter opens with `---` but is never closed",
43303
+ suggestion: "Add a matching `---` line (with no leading whitespace) after the last frontmatter field"
43304
+ };
43305
+ }
43231
43306
  function validateCursorMdc(file2) {
43232
43307
  const issues = [];
43233
43308
  const fm = parseFrontmatter(file2.content);
43309
+ if (fm.unclosed) {
43310
+ issues.push(unclosedFrontmatterIssue());
43311
+ return issues;
43312
+ }
43234
43313
  if (!fm.found) {
43235
43314
  issues.push({
43236
43315
  severity: "warning",
@@ -43277,13 +43356,13 @@ function validateCursorMdc(file2) {
43277
43356
  }
43278
43357
  if ("globs" in fm.fields) {
43279
43358
  const val = fm.fields["globs"];
43280
- if (val && !val.startsWith("[") && !val.startsWith('"') && !val.includes("*") && !val.includes("/")) {
43359
+ if (val && hasUnbalancedBracketsOrQuotes(val)) {
43281
43360
  issues.push({
43282
43361
  severity: "warning",
43283
43362
  check: "frontmatter",
43284
43363
  ruleId: "frontmatter/invalid-value",
43285
43364
  line: 1,
43286
- message: `Possibly invalid globs value: "${val}"`,
43365
+ message: `Possibly malformed globs value: "${val}"`,
43287
43366
  suggestion: 'globs should be a glob pattern like "src/**/*.ts" or an array like ["*.ts", "*.tsx"]'
43288
43367
  });
43289
43368
  }
@@ -43293,6 +43372,10 @@ function validateCursorMdc(file2) {
43293
43372
  function validateCopilotInstructions(file2) {
43294
43373
  const issues = [];
43295
43374
  const fm = parseFrontmatter(file2.content);
43375
+ if (fm.unclosed) {
43376
+ issues.push(unclosedFrontmatterIssue());
43377
+ return issues;
43378
+ }
43296
43379
  if (!fm.found) {
43297
43380
  issues.push({
43298
43381
  severity: "info",
@@ -43319,6 +43402,10 @@ function validateCopilotInstructions(file2) {
43319
43402
  function validateWindsurfRule(file2) {
43320
43403
  const issues = [];
43321
43404
  const fm = parseFrontmatter(file2.content);
43405
+ if (fm.unclosed) {
43406
+ issues.push(unclosedFrontmatterIssue());
43407
+ return issues;
43408
+ }
43322
43409
  if (!fm.found) {
43323
43410
  issues.push({
43324
43411
  severity: "info",
@@ -43490,6 +43577,10 @@ function isEnvVarRef(value) {
43490
43577
  function isKnownApiKey(value) {
43491
43578
  return API_KEY_PATTERNS.some((p2) => p2.test(value));
43492
43579
  }
43580
+ function nameSuggestsSecret(name) {
43581
+ const upper = name.toUpperCase();
43582
+ return SECRET_NAME_KEYWORDS.some((kw) => upper.includes(kw));
43583
+ }
43493
43584
  function isHighEntropySecret(value) {
43494
43585
  if (isEnvVarRef(value)) return false;
43495
43586
  return HIGH_ENTROPY_PATTERN.test(value);
@@ -43538,8 +43629,9 @@ async function checkMcpSecurity(config2, _projectRoot) {
43538
43629
  }
43539
43630
  }
43540
43631
  if (server2.env) {
43541
- for (const envValue of Object.values(server2.env)) {
43542
- if (!isEnvVarRef(envValue) && (isKnownApiKey(envValue) || isHighEntropySecret(envValue))) {
43632
+ for (const [envName, envValue] of Object.entries(server2.env)) {
43633
+ const isSecret = isKnownApiKey(envValue) || nameSuggestsSecret(envName) && isHighEntropySecret(envValue);
43634
+ if (!isEnvVarRef(envValue) && isSecret) {
43543
43635
  const envVar = deriveEnvVarName(server2.name, "API_KEY");
43544
43636
  issues.push({
43545
43637
  severity: "error",
@@ -43586,7 +43678,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43586
43678
  }
43587
43679
  return issues;
43588
43680
  }
43589
- var API_KEY_PATTERNS, ENV_VAR_REF, HIGH_ENTROPY_PATTERN, URL_SECRET_PARAMS;
43681
+ var API_KEY_PATTERNS, ENV_VAR_REF, HIGH_ENTROPY_PATTERN, URL_SECRET_PARAMS, SECRET_NAME_KEYWORDS;
43590
43682
  var init_security = __esm({
43591
43683
  "src/core/checks/mcp/security.ts"() {
43592
43684
  "use strict";
@@ -43615,6 +43707,22 @@ var init_security = __esm({
43615
43707
  ENV_VAR_REF = /\$\{[^}]+\}/;
43616
43708
  HIGH_ENTROPY_PATTERN = /^[A-Za-z0-9+/=_-]{21,}$/;
43617
43709
  URL_SECRET_PARAMS = /[?&](key|token|api_key|apikey|secret|password|access_token)=/i;
43710
+ SECRET_NAME_KEYWORDS = [
43711
+ "KEY",
43712
+ "TOKEN",
43713
+ "SECRET",
43714
+ "PASSWORD",
43715
+ "PASSWD",
43716
+ "PASS",
43717
+ "AUTH",
43718
+ "CREDENTIAL",
43719
+ "CREDENTIALS",
43720
+ "APIKEY",
43721
+ "PRIVATE",
43722
+ "SIGNING",
43723
+ "SESSION",
43724
+ "COOKIE"
43725
+ ];
43618
43726
  }
43619
43727
  });
43620
43728
 
@@ -44381,6 +44489,12 @@ function extractPaths(content) {
44381
44489
  paths.push(p2);
44382
44490
  }
44383
44491
  }
44492
+ for (const match of content.matchAll(BARE_FILE_PATH)) {
44493
+ const p2 = match[1].replace(/[)}\]]+$/, "");
44494
+ if (p2.length > 2 && !p2.startsWith("http")) {
44495
+ paths.push(p2);
44496
+ }
44497
+ }
44384
44498
  return [...new Set(paths)];
44385
44499
  }
44386
44500
  function parseFrontmatter2(content) {
@@ -44410,7 +44524,7 @@ function parseFrontmatter2(content) {
44410
44524
  };
44411
44525
  }
44412
44526
  async function parseMemoryFile(filePath, projectDir) {
44413
- const content = await readFile(filePath, "utf-8");
44527
+ const content = stripBom(await readFile(filePath, "utf-8"));
44414
44528
  const { name, description, type, body } = parseFrontmatter2(content);
44415
44529
  const referencedPaths = extractPaths(body);
44416
44530
  return {
@@ -44423,11 +44537,13 @@ async function parseMemoryFile(filePath, projectDir) {
44423
44537
  referencedPaths
44424
44538
  };
44425
44539
  }
44426
- var PATH_PATTERN2;
44540
+ var PATH_PATTERN2, BARE_FILE_PATH;
44427
44541
  var init_session_parser = __esm({
44428
44542
  "src/core/session-parser.ts"() {
44429
44543
  "use strict";
44544
+ init_fs();
44430
44545
  PATH_PATTERN2 = /(?:^|\s|['"`(])([.~/][^\s'"`),;:!?]+)/g;
44546
+ BARE_FILE_PATH = /(?:^|[\s`"'(])([\w][\w-]*(?:\/[\w.-]+)+\.[a-zA-Z0-9]{1,8})\b/g;
44431
44547
  }
44432
44548
  });
44433
44549
 
@@ -44459,30 +44575,46 @@ async function parseJsonlFiltered(filePath, filter) {
44459
44575
  }
44460
44576
  return results;
44461
44577
  }
44462
- async function readClaudeHistory() {
44463
- const historyPath = join5(home, ".claude", "history.jsonl");
44464
- return parseJsonlFiltered(historyPath, (entry) => {
44465
- if (!entry.display || !entry.project) return null;
44578
+ function pickField(entry, fields) {
44579
+ for (const f of fields) {
44580
+ const v2 = entry[f];
44581
+ if (typeof v2 === "string" && v2.length > 0) return v2;
44582
+ }
44583
+ return "";
44584
+ }
44585
+ async function readJsonlHistory(opts) {
44586
+ return parseJsonlFiltered(opts.historyPath, (parsed) => {
44587
+ if (!parsed || typeof parsed !== "object") return null;
44588
+ const entry = parsed;
44589
+ const display = pickField(entry, opts.displayFields);
44590
+ if (!display) return null;
44591
+ const project = pickField(entry, opts.projectFields);
44592
+ if (opts.requireProject && !project) return null;
44466
44593
  return {
44467
- display: entry.display,
44468
- timestamp: entry.timestamp || 0,
44469
- project: entry.project.replace(/\\/g, "/"),
44470
- sessionId: entry.sessionId || "",
44471
- provider: "claude-code"
44594
+ display,
44595
+ timestamp: typeof entry.timestamp === "number" ? entry.timestamp : 0,
44596
+ project: project.replace(/\\/g, "/"),
44597
+ sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
44598
+ provider: opts.provider
44472
44599
  };
44473
44600
  });
44474
44601
  }
44602
+ async function readClaudeHistory() {
44603
+ return readJsonlHistory({
44604
+ historyPath: join5(home, ".claude", "history.jsonl"),
44605
+ provider: "claude-code",
44606
+ displayFields: ["display"],
44607
+ projectFields: ["project"],
44608
+ requireProject: true
44609
+ });
44610
+ }
44475
44611
  async function readCodexHistory() {
44476
- const historyPath = join5(home, ".codex", "history.jsonl");
44477
- return parseJsonlFiltered(historyPath, (entry) => {
44478
- if (!entry.display && !entry.command) return null;
44479
- return {
44480
- display: entry.display || entry.command || "",
44481
- timestamp: entry.timestamp || 0,
44482
- project: (entry.project || entry.cwd || "").replace(/\\/g, "/"),
44483
- sessionId: entry.sessionId || "",
44484
- provider: "codex-cli"
44485
- };
44612
+ return readJsonlHistory({
44613
+ historyPath: join5(home, ".codex", "history.jsonl"),
44614
+ provider: "codex-cli",
44615
+ displayFields: ["display", "command"],
44616
+ projectFields: ["project", "cwd"],
44617
+ requireProject: false
44486
44618
  });
44487
44619
  }
44488
44620
  async function readClaudeMemories() {
@@ -44547,47 +44679,29 @@ async function detectSiblings(projectRoot) {
44547
44679
  if (orgMatch) currentOrg = orgMatch[1];
44548
44680
  } catch {
44549
44681
  }
44550
- const gitCandidates = candidates.filter((c3) => existsSync(join5(c3.fullPath, ".git")));
44551
- const results = await Promise.all(
44552
- gitCandidates.map(async (c3) => {
44553
- const sibling = { path: c3.entryPath.replace(/\\/g, "/"), name: c3.name };
44554
- try {
44555
- const git = simpleGit(c3.fullPath);
44556
- const remotes = await git.getRemotes(true);
44557
- const origin = remotes.find((r2) => r2.name === "origin");
44558
- if (origin?.refs?.fetch) {
44559
- sibling.gitRemoteUrl = origin.refs.fetch;
44560
- const orgMatch = origin.refs.fetch.match(/github\.com[:/]([^/]+)\//);
44561
- if (orgMatch) sibling.gitOrg = orgMatch[1];
44562
- }
44563
- } catch {
44564
- }
44565
- return sibling;
44566
- })
44567
- );
44682
+ const results = await Promise.all(candidates.map((c3) => resolveSibling(c3)));
44568
44683
  if (currentOrg) {
44569
- return results.filter((s) => s.gitOrg === currentOrg);
44684
+ return results.filter((s) => !s.gitOrg || s.gitOrg === currentOrg);
44570
44685
  }
44571
44686
  return results;
44572
44687
  }
44573
- const siblings = await Promise.all(
44574
- candidates.map(async (c3) => {
44575
- const sibling = { path: c3.entryPath.replace(/\\/g, "/"), name: c3.name };
44576
- try {
44577
- const git = simpleGit(c3.fullPath);
44578
- const remotes = await git.getRemotes(true);
44579
- const origin = remotes.find((r2) => r2.name === "origin");
44580
- if (origin?.refs?.fetch) {
44581
- sibling.gitRemoteUrl = origin.refs.fetch;
44582
- const orgMatch = origin.refs.fetch.match(/github\.com[:/]([^/]+)\//);
44583
- if (orgMatch) sibling.gitOrg = orgMatch[1];
44584
- }
44585
- } catch {
44586
- }
44587
- return sibling;
44588
- })
44589
- );
44590
- return siblings;
44688
+ return Promise.all(candidates.map((c3) => resolveSibling(c3)));
44689
+ }
44690
+ async function resolveSibling(c3) {
44691
+ const sibling = { path: c3.entryPath.replace(/\\/g, "/"), name: c3.name };
44692
+ if (!existsSync(join5(c3.fullPath, ".git"))) return sibling;
44693
+ try {
44694
+ const git = simpleGit(c3.fullPath);
44695
+ const remotes = await git.getRemotes(true);
44696
+ const origin = remotes.find((r2) => r2.name === "origin");
44697
+ if (origin?.refs?.fetch) {
44698
+ sibling.gitRemoteUrl = origin.refs.fetch;
44699
+ const orgMatch = origin.refs.fetch.match(/github\.com[:/]([^/]+)\//);
44700
+ if (orgMatch) sibling.gitOrg = orgMatch[1];
44701
+ }
44702
+ } catch {
44703
+ }
44704
+ return sibling;
44591
44705
  }
44592
44706
  async function scanSessionData(projectRoot) {
44593
44707
  const providers = detectProviders();
@@ -44707,48 +44821,57 @@ var init_missing_secret = __esm({
44707
44821
  });
44708
44822
 
44709
44823
  // src/core/checks/session/diverged-file.ts
44710
- import { readFile as readFile2 } from "node:fs/promises";
44824
+ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
44711
44825
  import { join as join6 } from "node:path";
44712
44826
  import { existsSync as existsSync2 } from "node:fs";
44713
- function calculateOverlap(a, b2) {
44714
- const linesA = new Set(
44715
- a.split("\n").map((l) => l.trim()).filter((l) => l.length > 3)
44716
- );
44717
- const linesB = new Set(
44718
- b2.split("\n").map((l) => l.trim()).filter((l) => l.length > 3)
44719
- );
44720
- if (linesA.size === 0 && linesB.size === 0) return 1;
44721
- if (linesA.size === 0 || linesB.size === 0) return 0;
44722
- let intersection2 = 0;
44723
- for (const line of linesA) {
44724
- if (linesB.has(line)) intersection2++;
44827
+ async function loadLineSet(absPath) {
44828
+ let mtimeMs;
44829
+ let size;
44830
+ try {
44831
+ const stats = await stat2(absPath);
44832
+ mtimeMs = stats.mtimeMs;
44833
+ size = stats.size;
44834
+ } catch {
44835
+ return null;
44725
44836
  }
44726
- const unionSize = linesA.size + linesB.size - intersection2;
44727
- return intersection2 / unionSize;
44837
+ const cached2 = lineSetCache.get(absPath);
44838
+ if (cached2 && cached2.mtimeMs === mtimeMs && cached2.size === size) {
44839
+ lineSetCache.delete(absPath);
44840
+ lineSetCache.set(absPath, cached2);
44841
+ return cached2.lineSet;
44842
+ }
44843
+ let content;
44844
+ try {
44845
+ content = stripBom(await readFile2(absPath, "utf-8"));
44846
+ } catch {
44847
+ return null;
44848
+ }
44849
+ const lineSet = toLineSet(content, MIN_TOKEN_LEN);
44850
+ lineSetCache.set(absPath, { mtimeMs, size, lineSet });
44851
+ if (lineSetCache.size > CACHE_MAX_ENTRIES) {
44852
+ const oldest = lineSetCache.keys().next().value;
44853
+ if (oldest !== void 0) lineSetCache.delete(oldest);
44854
+ }
44855
+ return lineSet;
44728
44856
  }
44729
44857
  async function checkDivergedFile(ctx) {
44730
44858
  const issues = [];
44731
44859
  for (const fileName of CANONICAL_FILES) {
44732
44860
  const currentPath = join6(ctx.currentProject, fileName);
44733
44861
  if (!existsSync2(currentPath)) continue;
44734
- let currentContent;
44735
- try {
44736
- currentContent = await readFile2(currentPath, "utf-8");
44737
- } catch {
44738
- continue;
44739
- }
44862
+ const currentLineSet = await loadLineSet(currentPath);
44863
+ if (!currentLineSet) continue;
44740
44864
  const diverged = [];
44741
44865
  for (const sib of ctx.siblings) {
44742
44866
  const sibPath = join6(sib.path, fileName);
44743
44867
  if (!existsSync2(sibPath)) continue;
44744
- try {
44745
- const sibContent = await readFile2(sibPath, "utf-8");
44746
- const overlap = calculateOverlap(currentContent, sibContent);
44747
- if (overlap >= 0.2 && overlap < 0.9) {
44748
- diverged.push({ sibling: sib.name, overlap: Math.round(overlap * 100) });
44749
- }
44750
- } catch {
44751
- continue;
44868
+ const sibLineSet = await loadLineSet(sibPath);
44869
+ if (!sibLineSet) continue;
44870
+ const overlap = jaccardSimilarityFromSets(currentLineSet, sibLineSet, {
44871
+ bothEmptyIsIdentical: true
44872
+ });
44873
+ if (overlap >= 0.2 && overlap < 0.9) {
44874
+ diverged.push({ sibling: sib.name, overlap: Math.round(overlap * 100) });
44752
44875
  }
44753
44876
  }
44754
44877
  if (diverged.length > 0) {
@@ -44766,10 +44889,12 @@ async function checkDivergedFile(ctx) {
44766
44889
  }
44767
44890
  return issues;
44768
44891
  }
44769
- var CANONICAL_FILES;
44892
+ var CANONICAL_FILES, MIN_TOKEN_LEN, CACHE_MAX_ENTRIES, lineSetCache;
44770
44893
  var init_diverged_file = __esm({
44771
44894
  "src/core/checks/session/diverged-file.ts"() {
44772
44895
  "use strict";
44896
+ init_similarity();
44897
+ init_fs();
44773
44898
  CANONICAL_FILES = [
44774
44899
  "release.sh",
44775
44900
  ".github/workflows/ci.yml",
@@ -44780,6 +44905,9 @@ var init_diverged_file = __esm({
44780
44905
  "tsconfig.json",
44781
44906
  ".gitignore"
44782
44907
  ];
44908
+ MIN_TOKEN_LEN = 3;
44909
+ CACHE_MAX_ENTRIES = 256;
44910
+ lineSetCache = /* @__PURE__ */ new Map();
44783
44911
  }
44784
44912
  });
44785
44913
 
@@ -44887,24 +45015,10 @@ var init_stale_memory = __esm({
44887
45015
  });
44888
45016
 
44889
45017
  // src/core/checks/session/duplicate-memory.ts
44890
- function calculateLineOverlap2(a, b2) {
44891
- const linesA = new Set(
44892
- a.split("\n").map((l) => l.trim()).filter((l) => l.length > 5)
44893
- );
44894
- const linesB = new Set(
44895
- b2.split("\n").map((l) => l.trim()).filter((l) => l.length > 5)
44896
- );
44897
- if (linesA.size === 0 || linesB.size === 0) return 0;
44898
- let intersection2 = 0;
44899
- for (const line of linesA) {
44900
- if (linesB.has(line)) intersection2++;
44901
- }
44902
- const unionSize = linesA.size + linesB.size - intersection2;
44903
- return intersection2 / unionSize;
44904
- }
44905
45018
  async function checkDuplicateMemory(ctx) {
44906
45019
  const issues = [];
44907
45020
  const reported = /* @__PURE__ */ new Set();
45021
+ const lineSets = ctx.memories.map((m) => toLineSet(m.content, MIN_TOKEN_LEN2));
44908
45022
  for (let i2 = 0; i2 < ctx.memories.length; i2++) {
44909
45023
  for (let j3 = i2 + 1; j3 < ctx.memories.length; j3++) {
44910
45024
  const a = ctx.memories[i2];
@@ -44914,7 +45028,7 @@ async function checkDuplicateMemory(ctx) {
44914
45028
  const bIsCurrent = projectDirMatchesPath(b2.projectDir, ctx.currentProject);
44915
45029
  if (!aIsCurrent && !bIsCurrent) continue;
44916
45030
  if (a.content.length < 50 || b2.content.length < 50) continue;
44917
- const overlap = calculateLineOverlap2(a.content, b2.content);
45031
+ const overlap = jaccardSimilarityFromSets(lineSets[i2], lineSets[j3]);
44918
45032
  if (overlap < 0.6) continue;
44919
45033
  const pairKey = [a.filePath, b2.filePath].sort().join("::");
44920
45034
  if (reported.has(pairKey)) continue;
@@ -44936,10 +45050,13 @@ async function checkDuplicateMemory(ctx) {
44936
45050
  }
44937
45051
  return issues;
44938
45052
  }
45053
+ var MIN_TOKEN_LEN2;
44939
45054
  var init_duplicate_memory = __esm({
44940
45055
  "src/core/checks/session/duplicate-memory.ts"() {
44941
45056
  "use strict";
44942
45057
  init_session_parser();
45058
+ init_similarity();
45059
+ MIN_TOKEN_LEN2 = 5;
44943
45060
  }
44944
45061
  });
44945
45062
 
@@ -45034,24 +45151,23 @@ var init_loop_detection = __esm({
45034
45151
  });
45035
45152
 
45036
45153
  // src/core/checks/session/memory-index-overflow.ts
45037
- import { readFile as readFile3, stat as stat2 } from "node:fs/promises";
45154
+ import { readFile as readFile3 } from "node:fs/promises";
45038
45155
  import { join as join8 } from "node:path";
45039
45156
  async function checkMemoryIndexOverflow(ctx) {
45040
45157
  const home2 = process.env.HOME || process.env.USERPROFILE || "";
45041
45158
  if (!home2) return [];
45042
45159
  const encoded = encodeProjectDir(ctx.currentProject);
45043
45160
  const memoryFile = join8(home2, ".claude", "projects", encoded, "memory", "MEMORY.md");
45044
- let stats;
45161
+ let content;
45045
45162
  try {
45046
- stats = await stat2(memoryFile);
45163
+ content = stripBom(await readFile3(memoryFile, "utf-8"));
45047
45164
  } catch {
45048
45165
  return [];
45049
45166
  }
45050
- const content = await readFile3(memoryFile, "utf-8").catch(() => "");
45051
45167
  if (!content) return [];
45052
45168
  const lines = content.split("\n");
45053
45169
  const lineCount = lines.length;
45054
- const byteSize = stats.size;
45170
+ const byteSize = Buffer.byteLength(content, "utf8");
45055
45171
  const issues = [];
45056
45172
  if (lineCount > MAX_LINES) {
45057
45173
  const excess = lineCount - MAX_LINES;
@@ -45084,6 +45200,7 @@ var init_memory_index_overflow = __esm({
45084
45200
  "src/core/checks/session/memory-index-overflow.ts"() {
45085
45201
  "use strict";
45086
45202
  init_session_parser();
45203
+ init_fs();
45087
45204
  MAX_LINES = 200;
45088
45205
  MAX_BYTES = 25 * 1024;
45089
45206
  }
@@ -45110,7 +45227,7 @@ async function findReleaseWorkflows(projectRoot) {
45110
45227
  continue;
45111
45228
  }
45112
45229
  try {
45113
- const content = await readFile4(join9(workflowDir, f), "utf-8");
45230
+ const content = stripBom(await readFile4(join9(workflowDir, f), "utf-8"));
45114
45231
  const nameMatch = content.match(/^name:\s*(.+)$/m);
45115
45232
  if (nameMatch && RELEASE_FILENAME_PATTERNS.some((p2) => p2.test(nameMatch[1]))) {
45116
45233
  releaseWorkflows.push(f);
@@ -45147,6 +45264,7 @@ var RELEASE_FILENAME_PATTERNS, RELEASE_DOC_PATTERNS;
45147
45264
  var init_ci_coverage = __esm({
45148
45265
  "src/core/checks/ci-coverage.ts"() {
45149
45266
  "use strict";
45267
+ init_fs();
45150
45268
  RELEASE_FILENAME_PATTERNS = [/release/i, /deploy/i, /publish/i, /\bcd\b/i];
45151
45269
  RELEASE_DOC_PATTERNS = [
45152
45270
  /release\s+(process|workflow|steps|via|by|using)/i,
@@ -45181,7 +45299,7 @@ async function findSecretUsages(projectRoot) {
45181
45299
  if (!(f.endsWith(".yml") || f.endsWith(".yaml"))) continue;
45182
45300
  let content;
45183
45301
  try {
45184
- content = await readFile5(join10(workflowDir, f), "utf-8");
45302
+ content = stripBom(await readFile5(join10(workflowDir, f), "utf-8"));
45185
45303
  } catch {
45186
45304
  continue;
45187
45305
  }
@@ -45198,7 +45316,7 @@ async function findSecretUsages(projectRoot) {
45198
45316
  }
45199
45317
  function contextMentionsSecret(files, secretName) {
45200
45318
  const escaped = secretName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
45201
- const pattern = new RegExp(`\\b${escaped.replace(/_/g, "[_\\s-]")}\\b`, "i");
45319
+ const pattern = new RegExp(`\\b${escaped.replace(/_/g, "[ _-]")}\\b`, "i");
45202
45320
  return files.some((f) => pattern.test(f.content));
45203
45321
  }
45204
45322
  async function checkCiSecrets(files, projectRoot) {
@@ -45222,6 +45340,7 @@ var BUILTIN_SECRETS, SECRETS_REGEX;
45222
45340
  var init_ci_secrets = __esm({
45223
45341
  "src/core/checks/ci-secrets.ts"() {
45224
45342
  "use strict";
45343
+ init_fs();
45225
45344
  BUILTIN_SECRETS = /* @__PURE__ */ new Set([
45226
45345
  "GITHUB_TOKEN",
45227
45346
  "ACTIONS_RUNTIME_TOKEN",
@@ -45242,7 +45361,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
45242
45361
  import { resolve as resolve8, dirname as dirname3 } from "node:path";
45243
45362
  import { fileURLToPath } from "node:url";
45244
45363
  function loadVersion() {
45245
- if (true) return "0.9.18";
45364
+ if (true) return "0.9.20";
45246
45365
  const __dir = dirname3(fileURLToPath(import.meta.url));
45247
45366
  const pkgPath = resolve8(__dir, "../package.json");
45248
45367
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
@@ -45266,6 +45385,11 @@ function hasMcphChecks(checks) {
45266
45385
  function hasSessionChecks(checks) {
45267
45386
  return checks.some((c3) => c3.startsWith("session-"));
45268
45387
  }
45388
+ function deriveChecksToRun(activeChecks, prefix, enabled, allChecks) {
45389
+ const filtered = activeChecks.filter((c3) => c3.startsWith(prefix));
45390
+ if (filtered.length > 0) return filtered;
45391
+ return enabled ? [...allChecks] : [];
45392
+ }
45269
45393
  async function runAudit(projectRoot, activeChecks, options = {}) {
45270
45394
  const fileResults = [];
45271
45395
  const shouldRunContextChecks = !options.mcpOnly && !options.mcphOnly && !options.sessionOnly;
@@ -45346,8 +45470,12 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45346
45470
  );
45347
45471
  mcpConfigs.push(...globalConfigs);
45348
45472
  }
45349
- const activeMcpChecks = activeChecks.filter((c3) => c3.startsWith("mcp-"));
45350
- const mcpChecksToRun = activeMcpChecks.length > 0 ? activeMcpChecks : options.mcp || options.mcpGlobal || options.mcpOnly ? ALL_MCP_CHECKS : [];
45473
+ const mcpChecksToRun = deriveChecksToRun(
45474
+ activeChecks,
45475
+ "mcp-",
45476
+ Boolean(options.mcp || options.mcpGlobal || options.mcpOnly),
45477
+ ALL_MCP_CHECKS
45478
+ );
45351
45479
  for (const config2 of mcpConfigs) {
45352
45480
  const checkPromises = [];
45353
45481
  if (mcpChecksToRun.includes("mcp-schema"))
@@ -45393,7 +45521,7 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45393
45521
  const projectMcphFiles = await scanForMcphConfigs(projectRoot);
45394
45522
  const mcphConfigs = await Promise.all(
45395
45523
  projectMcphFiles.map(
45396
- (f) => parseMchpConfig(
45524
+ (f) => parseMcphConfig(
45397
45525
  f,
45398
45526
  projectRoot,
45399
45527
  f.relativePath.endsWith(".mcph.local.json") ? "project-local" : "project"
@@ -45403,12 +45531,16 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45403
45531
  if (options.mcphGlobal) {
45404
45532
  const globalMcphFiles = await scanGlobalMcphConfigs();
45405
45533
  const globalMcphConfigs = await Promise.all(
45406
- globalMcphFiles.map((f) => parseMchpConfig(f, projectRoot, "global"))
45534
+ globalMcphFiles.map((f) => parseMcphConfig(f, projectRoot, "global"))
45407
45535
  );
45408
45536
  mcphConfigs.push(...globalMcphConfigs);
45409
45537
  }
45410
- const activeMcphChecks = activeChecks.filter((c3) => c3.startsWith("mcph-"));
45411
- const mcphChecksToRun = activeMcphChecks.length > 0 ? activeMcphChecks : options.mcph || options.mcphGlobal || options.mcphOnly ? ALL_MCPH_CHECKS : [];
45538
+ const mcphChecksToRun = deriveChecksToRun(
45539
+ activeChecks,
45540
+ "mcph-",
45541
+ Boolean(options.mcph || options.mcphGlobal || options.mcphOnly),
45542
+ ALL_MCPH_CHECKS
45543
+ );
45412
45544
  for (const config2 of mcphConfigs) {
45413
45545
  const checkPromises = [];
45414
45546
  if (mcphChecksToRun.includes("mcph-token-security")) {
@@ -45432,14 +45564,16 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45432
45564
  }
45433
45565
  const results = await Promise.all(checkPromises);
45434
45566
  const issues = results.flat();
45435
- for (const err of config2.parseErrors) {
45436
- issues.push({
45437
- severity: "error",
45438
- check: "mcph-schema-conformance",
45439
- ruleId: "mcph-config/parse-error",
45440
- line: 1,
45441
- message: err
45442
- });
45567
+ if (mcphChecksToRun.includes("mcph-schema-conformance")) {
45568
+ for (const err of config2.parseErrors) {
45569
+ issues.push({
45570
+ severity: "error",
45571
+ check: "mcph-schema-conformance",
45572
+ ruleId: "mcph-config/parse-error",
45573
+ line: 1,
45574
+ message: err
45575
+ });
45576
+ }
45443
45577
  }
45444
45578
  const lines = config2.content.split("\n").length;
45445
45579
  fileResults.push({
@@ -45475,15 +45609,13 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45475
45609
  sessionPromises.push(checkMemoryIndexOverflow(sessionCtx));
45476
45610
  const sessionResults = await Promise.all(sessionPromises);
45477
45611
  const sessionIssues = sessionResults.flat();
45478
- if (sessionIssues.length > 0) {
45479
- fileResults.push({
45480
- path: "~/.claude/ (session audit)",
45481
- isSymlink: false,
45482
- tokens: 0,
45483
- lines: 0,
45484
- issues: sessionIssues
45485
- });
45486
- }
45612
+ fileResults.push({
45613
+ path: "~/.claude/ (session audit)",
45614
+ isSymlink: false,
45615
+ tokens: 0,
45616
+ lines: 0,
45617
+ issues: sessionIssues
45618
+ });
45487
45619
  }
45488
45620
  }
45489
45621
  let estimatedWaste = 0;
@@ -46190,6 +46322,8 @@ function applyFixes(result, options = {}) {
46190
46322
  existing.push(fix);
46191
46323
  fixesByLine.set(fix.line, existing);
46192
46324
  }
46325
+ let perFileFixCount = 0;
46326
+ const perFileLogs = [];
46193
46327
  for (const [lineNum, lineFixes] of fixesByLine) {
46194
46328
  const lineIdx = lineNum - 1;
46195
46329
  if (lineIdx < 0 || lineIdx >= lines.length) continue;
@@ -46197,9 +46331,9 @@ function applyFixes(result, options = {}) {
46197
46331
  for (const fix of lineFixes) {
46198
46332
  if (line.includes(fix.oldText)) {
46199
46333
  line = line.replaceAll(fix.oldText, fix.newText);
46200
- totalFixes++;
46334
+ perFileFixCount++;
46201
46335
  const prefix = dryRun ? source_default.cyan(" Would fix") : source_default.green(" Fixed");
46202
- log(
46336
+ perFileLogs.push(
46203
46337
  prefix + ` Line ${fix.line}: ${source_default.dim(fix.oldText)} ${source_default.dim("->")} ${fix.newText}`
46204
46338
  );
46205
46339
  }
@@ -46223,6 +46357,8 @@ function applyFixes(result, options = {}) {
46223
46357
  fs7.writeFileSync(filePath, newContent, "utf-8");
46224
46358
  }
46225
46359
  filesModified.push(filePath);
46360
+ totalFixes += perFileFixCount;
46361
+ for (const m of perFileLogs) log(m);
46226
46362
  }
46227
46363
  }
46228
46364
  return { totalFixes, filesModified };
@@ -46236,11 +46372,22 @@ var init_fixer = __esm({
46236
46372
 
46237
46373
  // src/mcp/server.ts
46238
46374
  var server_exports = {};
46375
+ __export(server_exports, {
46376
+ server: () => server,
46377
+ startServer: () => startServer
46378
+ });
46239
46379
  import * as path10 from "node:path";
46380
+ function describeDisallowed(rawPath) {
46381
+ const m = rawPath.match(PATH_DISALLOWED);
46382
+ if (!m) return "unknown";
46383
+ return JSON.stringify(m[0]);
46384
+ }
46240
46385
  function validateProjectPath(rawPath) {
46241
46386
  if (!rawPath) return process.cwd();
46242
46387
  if (PATH_DISALLOWED.test(rawPath)) {
46243
- throw new Error("projectPath contains disallowed characters");
46388
+ throw new Error(
46389
+ `projectPath contains disallowed character ${describeDisallowed(rawPath)} (control chars and shell metacharacters are rejected)`
46390
+ );
46244
46391
  }
46245
46392
  const resolved = path10.resolve(rawPath);
46246
46393
  if (!isDirectory(resolved)) {
@@ -46250,12 +46397,28 @@ function validateProjectPath(rawPath) {
46250
46397
  }
46251
46398
  function validateFilePathInput(rawPath) {
46252
46399
  if (PATH_DISALLOWED.test(rawPath)) {
46253
- throw new Error("path contains disallowed characters");
46400
+ throw new Error(
46401
+ `path contains disallowed character ${describeDisallowed(rawPath)} (control chars and shell metacharacters are rejected)`
46402
+ );
46254
46403
  }
46255
46404
  }
46256
- var contextCheckEnum, mcpCheckEnum, sessionCheckEnum, PATH_DISALLOWED, server, transport;
46405
+ function resolveWithinRoot(filePath, root) {
46406
+ const resolvedRoot = path10.resolve(root);
46407
+ const resolved = path10.resolve(resolvedRoot, filePath);
46408
+ const rel = path10.relative(resolvedRoot, resolved);
46409
+ if (rel.startsWith("..") || path10.isAbsolute(rel)) {
46410
+ throw new Error("path escapes the project root");
46411
+ }
46412
+ return resolved;
46413
+ }
46414
+ async function startServer() {
46415
+ keepEncoderAlive(true);
46416
+ const transport = new StdioServerTransport();
46417
+ await server.connect(transport);
46418
+ }
46419
+ var contextCheckEnum, mcpCheckEnum, mcphCheckEnum, sessionCheckEnum, PATH_DISALLOWED, server;
46257
46420
  var init_server3 = __esm({
46258
- async "src/mcp/server.ts"() {
46421
+ "src/mcp/server.ts"() {
46259
46422
  "use strict";
46260
46423
  init_mcp();
46261
46424
  init_stdio2();
@@ -46272,6 +46435,7 @@ var init_server3 = __esm({
46272
46435
  init_version2();
46273
46436
  contextCheckEnum = external_exports3.enum(ALL_CHECKS);
46274
46437
  mcpCheckEnum = external_exports3.enum(ALL_MCP_CHECKS);
46438
+ mcphCheckEnum = external_exports3.enum(ALL_MCPH_CHECKS);
46275
46439
  sessionCheckEnum = external_exports3.enum(ALL_SESSION_CHECKS);
46276
46440
  PATH_DISALLOWED = /[\n\r\t;`|]|\$\(|\$\{/;
46277
46441
  server = new McpServer({
@@ -46328,7 +46492,7 @@ var init_server3 = __esm({
46328
46492
  try {
46329
46493
  validateFilePathInput(filePath);
46330
46494
  const root = validateProjectPath(projectPath);
46331
- const resolved = path10.resolve(root, filePath);
46495
+ const resolved = resolveWithinRoot(filePath, root);
46332
46496
  const result = {
46333
46497
  path: filePath,
46334
46498
  exists: fileExists(resolved) || isDirectory(resolved)
@@ -46493,6 +46657,48 @@ var init_server3 = __esm({
46493
46657
  }
46494
46658
  }
46495
46659
  );
46660
+ server.tool(
46661
+ "ctxlint_mcph_audit",
46662
+ "Lint .mcph.json (the @yawlabs/mcph CLI config) files. Checks for PAT (mcp_pat_*) leakage in git-tracked project-scope files, environment-variable posture, plaintext HTTP apiBase to public hosts, schema drift, allow/deny list conflicts and duplicates, and machine-local override files not covered by .gitignore. Distinct from ctxlint_mcp_audit, which lints client-side .mcp.json server lists.",
46663
+ {
46664
+ projectPath: external_exports3.string().optional().describe("Path to the project root. Defaults to current working directory."),
46665
+ checks: external_exports3.array(mcphCheckEnum).optional().describe("Specific mcph checks to run (default: all mcph-* checks)."),
46666
+ includeGlobal: external_exports3.boolean().optional().describe("Also scan ~/.mcph.json (user-global config)."),
46667
+ strictEnvToken: external_exports3.boolean().optional().describe(
46668
+ "Upgrade mcph-config/prefer-env-token from warning to error (env-var-only posture)."
46669
+ )
46670
+ },
46671
+ {
46672
+ readOnlyHint: true,
46673
+ destructiveHint: false,
46674
+ idempotentHint: true,
46675
+ openWorldHint: false
46676
+ },
46677
+ async ({ projectPath, checks, includeGlobal, strictEnvToken }) => {
46678
+ try {
46679
+ const root = validateProjectPath(projectPath);
46680
+ const activeChecks = checks?.length ? checks : ALL_MCPH_CHECKS;
46681
+ const result = await runAudit(root, activeChecks, {
46682
+ mcph: true,
46683
+ mcphOnly: true,
46684
+ mcphGlobal: includeGlobal || false,
46685
+ mcphStrictEnvToken: strictEnvToken || false
46686
+ });
46687
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
46688
+ } catch (err) {
46689
+ const msg = err instanceof Error ? err.message : String(err);
46690
+ return {
46691
+ content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
46692
+ isError: true
46693
+ };
46694
+ } finally {
46695
+ freeEncoder();
46696
+ resetGit();
46697
+ resetPathsCache();
46698
+ resetPackageJsonCache();
46699
+ }
46700
+ }
46701
+ );
46496
46702
  server.tool(
46497
46703
  "ctxlint_session_audit",
46498
46704
  "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.",
@@ -46529,9 +46735,6 @@ var init_server3 = __esm({
46529
46735
  }
46530
46736
  }
46531
46737
  );
46532
- keepEncoderAlive(true);
46533
- transport = new StdioServerTransport();
46534
- await server.connect(transport);
46535
46738
  }
46536
46739
  });
46537
46740
 
@@ -53008,6 +53211,26 @@ var init_ora = __esm({
53008
53211
  });
53009
53212
 
53010
53213
  // src/core/reporter.ts
53214
+ function classifyFile(f) {
53215
+ if (f.path === "(project)") return "context";
53216
+ if (f.path === "(mcp)") return "mcp";
53217
+ if (f.path === "(mcph)") return "mcph";
53218
+ if (f.path.includes("session audit")) return "session";
53219
+ for (const issue2 of f.issues) {
53220
+ if (issue2.check.startsWith("session-")) return "session";
53221
+ if (issue2.check.startsWith("mcph-")) return "mcph";
53222
+ if (issue2.check.startsWith("mcp-")) return "mcp";
53223
+ }
53224
+ const norm = f.path.replace(/\\/g, "/");
53225
+ if (norm.endsWith(".mcph.json") || norm.endsWith(".mcph.local.json")) return "mcph";
53226
+ if (norm === ".mcp.json" || norm.endsWith("/.mcp.json") || norm.endsWith("/mcp.json") || norm.includes("/mcpServers/") || norm.endsWith("/.claude.json") || norm.endsWith(".claude/settings.json") || norm.endsWith("claude_desktop_config.json")) {
53227
+ return "mcp";
53228
+ }
53229
+ return "context";
53230
+ }
53231
+ function isSyntheticBucket(p2) {
53232
+ return p2.startsWith("(") || p2.includes("session audit");
53233
+ }
53011
53234
  function formatText(result, verbose = false) {
53012
53235
  const lines = [];
53013
53236
  lines.push("");
@@ -53015,30 +53238,45 @@ function formatText(result, verbose = false) {
53015
53238
  lines.push("");
53016
53239
  lines.push(`Scanning ${result.projectRoot}...`);
53017
53240
  lines.push("");
53018
- const isMcpFile = (f) => f.issues.some((i2) => i2.check.startsWith("mcp-"));
53019
- const contextFiles = result.files.filter((f) => !isMcpFile(f));
53020
- const mcpFiles = result.files.filter((f) => isMcpFile(f));
53241
+ const groups = {
53242
+ context: [],
53243
+ mcp: [],
53244
+ mcph: [],
53245
+ session: []
53246
+ };
53247
+ for (const f of result.files) {
53248
+ groups[classifyFile(f)].push(f);
53249
+ }
53021
53250
  const totalTokens = result.summary.totalTokens;
53022
- if (contextFiles.length > 0) {
53251
+ let renderedAnySummary = false;
53252
+ const contextReal = groups.context.filter((f) => !isSyntheticBucket(f.path));
53253
+ if (contextReal.length > 0) {
53023
53254
  lines.push(
53024
- `Found ${contextFiles.length} context file${contextFiles.length !== 1 ? "s" : ""} (${totalTokens.toLocaleString()} tokens total)`
53255
+ `Found ${contextReal.length} context file${contextReal.length !== 1 ? "s" : ""} (${totalTokens.toLocaleString()} tokens total)`
53025
53256
  );
53026
- for (const file2 of contextFiles) {
53257
+ for (const file2 of contextReal) {
53027
53258
  let desc = ` ${file2.path} (${file2.tokens.toLocaleString()} tokens, ${file2.lines} lines)`;
53028
53259
  if (file2.isSymlink && file2.symlinkTarget) {
53029
53260
  desc = ` ${file2.path} ${source_default.dim(`-> ${file2.symlinkTarget} (symlink)`)}`;
53030
53261
  }
53031
53262
  lines.push(desc);
53032
53263
  }
53264
+ renderedAnySummary = true;
53033
53265
  }
53034
- if (mcpFiles.length > 0) {
53035
- if (contextFiles.length > 0) lines.push("");
53036
- lines.push(`Found ${mcpFiles.length} MCP config${mcpFiles.length !== 1 ? "s" : ""}`);
53037
- for (const file2 of mcpFiles) {
53038
- lines.push(` ${file2.path}`);
53039
- }
53266
+ for (const g of ["mcp", "mcph"]) {
53267
+ const real = groups[g].filter((f) => !isSyntheticBucket(f.path));
53268
+ if (real.length === 0) continue;
53269
+ if (renderedAnySummary) lines.push("");
53270
+ lines.push(`Found ${real.length} ${GROUP_SUMMARY_NOUNS[g]}${real.length !== 1 ? "s" : ""}`);
53271
+ for (const file2 of real) lines.push(` ${file2.path}`);
53272
+ renderedAnySummary = true;
53040
53273
  }
53041
- if (contextFiles.length === 0 && mcpFiles.length === 0) {
53274
+ if (groups.session.length > 0) {
53275
+ if (renderedAnySummary) lines.push("");
53276
+ lines.push("Session audit scanned");
53277
+ renderedAnySummary = true;
53278
+ }
53279
+ if (!renderedAnySummary) {
53042
53280
  lines.push(`Found ${result.files.length} file${result.files.length !== 1 ? "s" : ""}`);
53043
53281
  }
53044
53282
  lines.push("");
@@ -53057,16 +53295,13 @@ function formatText(result, verbose = false) {
53057
53295
  lines.push("");
53058
53296
  }
53059
53297
  };
53060
- if (contextFiles.length > 0 && mcpFiles.length > 0) {
53061
- const contextWithIssues = contextFiles.filter((f) => f.issues.length > 0 || verbose);
53062
- const mcpWithIssues = mcpFiles.filter((f) => f.issues.length > 0 || verbose);
53063
- if (contextWithIssues.length > 0) {
53064
- lines.push(source_default.bold("Context Files"));
53065
- renderFileGroup(contextFiles);
53066
- }
53067
- if (mcpWithIssues.length > 0) {
53068
- lines.push(source_default.bold("MCP Configs"));
53069
- renderFileGroup(mcpFiles);
53298
+ const groupsToRender = GROUP_ORDER.filter(
53299
+ (g) => groups[g].some((f) => f.issues.length > 0 || verbose)
53300
+ );
53301
+ if (groupsToRender.length > 1) {
53302
+ for (const g of groupsToRender) {
53303
+ lines.push(source_default.bold(GROUP_LABELS[g]));
53304
+ renderFileGroup(groups[g]);
53070
53305
  }
53071
53306
  } else {
53072
53307
  renderFileGroup(result.files);
@@ -53282,6 +53517,31 @@ function buildRuleDescriptors() {
53282
53517
  shortDescription: { text: "Redundant MCP config entry" },
53283
53518
  helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
53284
53519
  },
53520
+ {
53521
+ id: "ctxlint/mcph-token-security",
53522
+ shortDescription: { text: "mcp.hosting PAT leakage or env-var posture" },
53523
+ helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
53524
+ },
53525
+ {
53526
+ id: "ctxlint/mcph-apibase",
53527
+ shortDescription: { text: "mcph apiBase URL validation" },
53528
+ helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
53529
+ },
53530
+ {
53531
+ id: "ctxlint/mcph-schema-conformance",
53532
+ shortDescription: { text: "mcph config unknown field or stale schema version" },
53533
+ helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
53534
+ },
53535
+ {
53536
+ id: "ctxlint/mcph-lists",
53537
+ shortDescription: { text: "mcph allow/deny list conflict or duplicate entry" },
53538
+ helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
53539
+ },
53540
+ {
53541
+ id: "ctxlint/mcph-gitignore",
53542
+ shortDescription: { text: "mcph machine-local file not covered by .gitignore" },
53543
+ helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
53544
+ },
53285
53545
  {
53286
53546
  id: "ctxlint/session-missing-secret",
53287
53547
  shortDescription: { text: "GitHub secret set on sibling repos but missing here" },
@@ -53335,10 +53595,24 @@ function formatIssue(issue2) {
53335
53595
  }
53336
53596
  return line;
53337
53597
  }
53598
+ var GROUP_ORDER, GROUP_LABELS, GROUP_SUMMARY_NOUNS;
53338
53599
  var init_reporter = __esm({
53339
53600
  "src/core/reporter.ts"() {
53340
53601
  "use strict";
53341
53602
  init_source();
53603
+ GROUP_ORDER = ["context", "mcp", "mcph", "session"];
53604
+ GROUP_LABELS = {
53605
+ context: "Context Files",
53606
+ mcp: "MCP Configs",
53607
+ mcph: "mcph Configs",
53608
+ session: "Session Audit"
53609
+ };
53610
+ GROUP_SUMMARY_NOUNS = {
53611
+ context: "context file",
53612
+ mcp: "MCP config",
53613
+ mcph: "mcph config",
53614
+ session: "session audit"
53615
+ };
53342
53616
  }
53343
53617
  });
53344
53618
 
@@ -53437,7 +53711,7 @@ function loadConfig(projectRoot) {
53437
53711
  const filePath = path11.join(projectRoot, filename);
53438
53712
  let content;
53439
53713
  try {
53440
- content = fs8.readFileSync(filePath, "utf-8");
53714
+ content = stripBom(fs8.readFileSync(filePath, "utf-8"));
53441
53715
  } catch {
53442
53716
  continue;
53443
53717
  }
@@ -53448,7 +53722,7 @@ function loadConfig(projectRoot) {
53448
53722
  function loadConfigFromExplicitPath(configPath) {
53449
53723
  let content;
53450
53724
  try {
53451
- content = fs8.readFileSync(configPath, "utf-8");
53725
+ content = stripBom(fs8.readFileSync(configPath, "utf-8"));
53452
53726
  } catch (err) {
53453
53727
  const detail = err instanceof Error ? err.message : String(err);
53454
53728
  throw new Error(`could not load config from ${configPath}: ${detail}`, { cause: err });
@@ -53460,6 +53734,7 @@ var init_config2 = __esm({
53460
53734
  "src/core/config.ts"() {
53461
53735
  "use strict";
53462
53736
  import_fast_levenshtein2 = __toESM(require_levenshtein(), 1);
53737
+ init_fs();
53463
53738
  levenshtein2 = import_fast_levenshtein2.default.get;
53464
53739
  KNOWN_CONFIG_KEYS = [
53465
53740
  "checks",
@@ -53511,74 +53786,7 @@ async function runCli() {
53511
53786
  false
53512
53787
  ).option("--session", "Run session audit checks (cross-project consistency)", false).option("--session-only", "Run only session checks, skip context and MCP checks", false).option("--watch", "Re-lint on context file changes", false).action(async (projectPath, opts) => {
53513
53788
  const resolvedPath = path12.resolve(projectPath);
53514
- const configPath = opts.config ? path12.resolve(opts.config) : void 0;
53515
- const config2 = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
53516
- const mcpGlobal = opts.mcpGlobal || config2?.mcpGlobal || false;
53517
- const mcpOnly = opts.mcpOnly || config2?.mcpOnly || false;
53518
- const mcpFlag = opts.mcp || mcpGlobal || mcpOnly || config2?.mcp || false;
53519
- const mcphGlobal = opts.mcphGlobal || config2?.mcphGlobal || false;
53520
- const mcphOnly = opts.mcphOnly || config2?.mcphOnly || false;
53521
- const mcphFlag = opts.mcph || mcphGlobal || mcphOnly || config2?.mcph || false;
53522
- const mcphStrictEnvToken = opts.mcphStrictEnvToken || config2?.mcphStrictEnvToken || false;
53523
- const sessionOnly = opts.sessionOnly || config2?.sessionOnly || false;
53524
- const sessionFlag = opts.session || sessionOnly || config2?.session || false;
53525
- let explicitChecks = opts.checks ? validateCheckNames(
53526
- opts.checks.split(",").map((c3) => c3.trim()),
53527
- "--checks"
53528
- ) : null;
53529
- if (explicitChecks?.length === 0) explicitChecks = null;
53530
- const hasMcpInChecks = explicitChecks?.some((c3) => c3.startsWith("mcp-")) || false;
53531
- const hasMcphInChecks = explicitChecks?.some((c3) => c3.startsWith("mcph-")) || false;
53532
- const hasSessionInChecks = explicitChecks?.some((c3) => c3.startsWith("session-")) || false;
53533
- const effectiveMcp = mcpFlag || hasMcpInChecks;
53534
- const effectiveMcph = mcphFlag || hasMcphInChecks;
53535
- const effectiveSession = sessionFlag || sessionOnly || hasSessionInChecks;
53536
- let checks;
53537
- if (explicitChecks) {
53538
- checks = explicitChecks;
53539
- } else if (sessionOnly) {
53540
- checks = ALL_SESSION_CHECKS;
53541
- } else if (mcpOnly) {
53542
- checks = ALL_MCP_CHECKS;
53543
- } else if (mcphOnly) {
53544
- checks = ALL_MCPH_CHECKS;
53545
- } else {
53546
- const base = config2?.checks || ALL_CHECKS;
53547
- checks = [
53548
- ...base,
53549
- ...effectiveMcp ? ALL_MCP_CHECKS : [],
53550
- ...effectiveMcph ? ALL_MCPH_CHECKS : [],
53551
- ...effectiveSession ? ALL_SESSION_CHECKS : []
53552
- ];
53553
- }
53554
- const options = {
53555
- projectPath: resolvedPath,
53556
- checks,
53557
- strict: opts.strict || config2?.strict || false,
53558
- format: opts.format,
53559
- verbose: opts.verbose,
53560
- fix: opts.fix,
53561
- ignore: opts.ignore ? validateCheckNames(
53562
- opts.ignore.split(",").map((c3) => c3.trim()),
53563
- "--ignore"
53564
- ) : config2?.ignore || [],
53565
- tokensOnly: opts.tokens,
53566
- quiet: opts.quiet,
53567
- depth: Math.max(0, Math.min(parseInt(opts.depth, 10) || 2, 10)),
53568
- mcp: effectiveMcp,
53569
- mcpOnly,
53570
- mcpGlobal,
53571
- mcph: effectiveMcph,
53572
- mcphOnly,
53573
- mcphGlobal,
53574
- mcphStrictEnvToken,
53575
- session: effectiveSession,
53576
- sessionOnly
53577
- };
53578
- if (config2?.tokenThresholds) {
53579
- setTokenThresholds(config2.tokenThresholds);
53580
- }
53581
- const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
53789
+ const { config: config2, options, activeChecks } = resolveSession(resolvedPath, opts);
53582
53790
  const spinner = options.format === "text" && !options.quiet ? ora("Scanning for context files...").start() : void 0;
53583
53791
  try {
53584
53792
  if (spinner) spinner.text = "Running checks...";
@@ -53706,7 +53914,16 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53706
53914
  path12.join(resolvedPath, "GEMINI.md"),
53707
53915
  path12.join(resolvedPath, "replit.md"),
53708
53916
  path12.join(resolvedPath, "package.json"),
53709
- path12.join(resolvedPath, ".mcp.json")
53917
+ path12.join(resolvedPath, ".mcp.json"),
53918
+ // mcph configs — without these, edits to the .mcph.json the user is
53919
+ // actively iterating on don't trigger a re-lint.
53920
+ path12.join(resolvedPath, ".mcph.json"),
53921
+ path12.join(resolvedPath, ".mcph.local.json"),
53922
+ // ctxlint config — re-resolve on edit so threshold changes,
53923
+ // ignore-list edits, and check-list overrides take effect without
53924
+ // restarting the watcher.
53925
+ path12.join(resolvedPath, ".ctxlintrc"),
53926
+ path12.join(resolvedPath, ".ctxlintrc.json")
53710
53927
  ];
53711
53928
  const watchDirs = [
53712
53929
  path12.join(resolvedPath, ".claude"),
@@ -53727,30 +53944,46 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53727
53944
  if (debounceTimer) clearTimeout(debounceTimer);
53728
53945
  debounceTimer = setTimeout(async () => {
53729
53946
  if (process.stdout.isTTY) console.clear();
53947
+ let liveConfig = config2;
53948
+ let liveOptions = options;
53949
+ let liveActiveChecks = activeChecks;
53950
+ try {
53951
+ const resolved = resolveSession(resolvedPath, opts);
53952
+ liveConfig = resolved.config;
53953
+ liveOptions = resolved.options;
53954
+ liveActiveChecks = resolved.activeChecks;
53955
+ } catch (err) {
53956
+ console.error("Error reloading config:", err instanceof Error ? err.message : err);
53957
+ }
53730
53958
  try {
53731
- const result = await runAudit(resolvedPath, activeChecks, {
53732
- depth: options.depth,
53733
- extraPatterns: config2?.contextFiles,
53734
- mcp: options.mcp,
53735
- mcpGlobal: options.mcpGlobal,
53736
- mcpOnly: options.mcpOnly,
53737
- mcph: options.mcph,
53738
- mcphGlobal: options.mcphGlobal,
53739
- mcphOnly: options.mcphOnly,
53740
- mcphStrictEnvToken: options.mcphStrictEnvToken,
53741
- session: options.session,
53742
- sessionOnly: options.sessionOnly
53959
+ if (liveConfig?.tokenThresholds) {
53960
+ setTokenThresholds(liveConfig.tokenThresholds);
53961
+ } else {
53962
+ resetTokenThresholds();
53963
+ }
53964
+ const result = await runAudit(resolvedPath, liveActiveChecks, {
53965
+ depth: liveOptions.depth,
53966
+ extraPatterns: liveConfig?.contextFiles,
53967
+ mcp: liveOptions.mcp,
53968
+ mcpGlobal: liveOptions.mcpGlobal,
53969
+ mcpOnly: liveOptions.mcpOnly,
53970
+ mcph: liveOptions.mcph,
53971
+ mcphGlobal: liveOptions.mcphGlobal,
53972
+ mcphOnly: liveOptions.mcphOnly,
53973
+ mcphStrictEnvToken: liveOptions.mcphStrictEnvToken,
53974
+ session: liveOptions.session,
53975
+ sessionOnly: liveOptions.sessionOnly
53743
53976
  });
53744
53977
  if (result.files.length === 0) {
53745
53978
  console.log("\nNo context files found.\n");
53746
- } else if (options.tokensOnly) {
53979
+ } else if (liveOptions.tokensOnly) {
53747
53980
  console.log(formatTokenReport(result));
53748
- } else if (options.format === "json") {
53981
+ } else if (liveOptions.format === "json") {
53749
53982
  console.log(formatJson(result));
53750
- } else if (options.format === "sarif") {
53983
+ } else if (liveOptions.format === "sarif") {
53751
53984
  console.log(formatSarif(result));
53752
53985
  } else {
53753
- console.log(formatText(result, options.verbose));
53986
+ console.log(formatText(result, liveOptions.verbose));
53754
53987
  }
53755
53988
  } catch (err) {
53756
53989
  console.error("Error:", err instanceof Error ? err.message : err);
@@ -53760,7 +53993,6 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53760
53993
  resetPathsCache();
53761
53994
  resetPackageJsonCache();
53762
53995
  resetTokenThresholds();
53763
- if (config2?.tokenThresholds) setTokenThresholds(config2.tokenThresholds);
53764
53996
  }
53765
53997
  console.log(chalk2.dim("\nWatching for changes... (Ctrl+C to stop)\n"));
53766
53998
  }, 300);
@@ -53839,6 +54071,77 @@ async function promptYesNo(question) {
53839
54071
  rl.close();
53840
54072
  }
53841
54073
  }
54074
+ function resolveSession(resolvedPath, opts) {
54075
+ const configPath = opts.config ? path12.resolve(opts.config) : void 0;
54076
+ const config2 = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
54077
+ const mcpGlobal = opts.mcpGlobal || config2?.mcpGlobal || false;
54078
+ const mcpOnly = opts.mcpOnly || config2?.mcpOnly || false;
54079
+ const mcpFlag = opts.mcp || mcpGlobal || mcpOnly || config2?.mcp || false;
54080
+ const mcphGlobal = opts.mcphGlobal || config2?.mcphGlobal || false;
54081
+ const mcphOnly = opts.mcphOnly || config2?.mcphOnly || false;
54082
+ const mcphFlag = opts.mcph || mcphGlobal || mcphOnly || config2?.mcph || false;
54083
+ const mcphStrictEnvToken = opts.mcphStrictEnvToken || config2?.mcphStrictEnvToken || false;
54084
+ const sessionOnly = opts.sessionOnly || config2?.sessionOnly || false;
54085
+ const sessionFlag = opts.session || sessionOnly || config2?.session || false;
54086
+ let explicitChecks = opts.checks ? validateCheckNames(
54087
+ opts.checks.split(",").map((c3) => c3.trim()),
54088
+ "--checks"
54089
+ ) : null;
54090
+ if (explicitChecks?.length === 0) explicitChecks = null;
54091
+ const hasMcpInChecks = explicitChecks?.some((c3) => c3.startsWith("mcp-") && !c3.startsWith("mcph-")) || false;
54092
+ const hasMcphInChecks = explicitChecks?.some((c3) => c3.startsWith("mcph-")) || false;
54093
+ const hasSessionInChecks = explicitChecks?.some((c3) => c3.startsWith("session-")) || false;
54094
+ const effectiveMcp = mcpFlag || hasMcpInChecks;
54095
+ const effectiveMcph = mcphFlag || hasMcphInChecks;
54096
+ const effectiveSession = sessionFlag || sessionOnly || hasSessionInChecks;
54097
+ let checks;
54098
+ if (explicitChecks) {
54099
+ checks = explicitChecks;
54100
+ } else if (sessionOnly) {
54101
+ checks = ALL_SESSION_CHECKS;
54102
+ } else if (mcpOnly) {
54103
+ checks = ALL_MCP_CHECKS;
54104
+ } else if (mcphOnly) {
54105
+ checks = ALL_MCPH_CHECKS;
54106
+ } else {
54107
+ const base = config2?.checks || ALL_CHECKS;
54108
+ checks = [
54109
+ ...base,
54110
+ ...effectiveMcp ? ALL_MCP_CHECKS : [],
54111
+ ...effectiveMcph ? ALL_MCPH_CHECKS : [],
54112
+ ...effectiveSession ? ALL_SESSION_CHECKS : []
54113
+ ];
54114
+ }
54115
+ const options = {
54116
+ projectPath: resolvedPath,
54117
+ checks,
54118
+ strict: opts.strict || config2?.strict || false,
54119
+ format: opts.format,
54120
+ verbose: opts.verbose,
54121
+ fix: opts.fix,
54122
+ ignore: opts.ignore ? validateCheckNames(
54123
+ opts.ignore.split(",").map((c3) => c3.trim()),
54124
+ "--ignore"
54125
+ ) : config2?.ignore || [],
54126
+ tokensOnly: opts.tokens,
54127
+ quiet: opts.quiet,
54128
+ depth: Math.max(0, Math.min(parseInt(opts.depth, 10) || 2, 10)),
54129
+ mcp: effectiveMcp,
54130
+ mcpOnly,
54131
+ mcpGlobal,
54132
+ mcph: effectiveMcph,
54133
+ mcphOnly,
54134
+ mcphGlobal,
54135
+ mcphStrictEnvToken,
54136
+ session: effectiveSession,
54137
+ sessionOnly
54138
+ };
54139
+ if (config2?.tokenThresholds) {
54140
+ setTokenThresholds(config2.tokenThresholds);
54141
+ }
54142
+ const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
54143
+ return { config: config2, options, activeChecks };
54144
+ }
53842
54145
  function loadConfigFromPath(configPath) {
53843
54146
  try {
53844
54147
  return loadConfigFromExplicitPath(configPath);
@@ -53876,7 +54179,8 @@ var init_cli = __esm({
53876
54179
  // src/index.ts
53877
54180
  var args = process.argv.slice(2);
53878
54181
  if (args[0] === "serve" || args.includes("--mcp-server")) {
53879
- await init_server3().then(() => server_exports);
54182
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server3(), server_exports));
54183
+ await startServer2();
53880
54184
  } else {
53881
54185
  const { runCli: runCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
53882
54186
  await runCli2();