@yawlabs/ctxlint 0.9.19 → 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.
Files changed (3) hide show
  1. package/README.md +99 -57
  2. package/dist/index.js +428 -247
  3. package/package.json +1 -1
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";
@@ -41746,9 +41745,9 @@ var init_main = __esm({
41746
41745
  });
41747
41746
 
41748
41747
  // src/core/mcph-parser.ts
41749
- async function parseMchpConfig(file2, projectRoot, scopeOverride) {
41748
+ async function parseMcphConfig(file2, projectRoot, scopeOverride) {
41750
41749
  const content = readFileContent(file2.absolutePath);
41751
- const scope = scopeOverride ?? detectScope2(file2.relativePath);
41750
+ const scope = scopeOverride ?? detectScope(file2.relativePath);
41752
41751
  const isGitTracked = await checkGitTracked2(file2.absolutePath, projectRoot);
41753
41752
  const isGitignored = await checkGitignored(file2.absolutePath, projectRoot);
41754
41753
  const result = {
@@ -41806,7 +41805,7 @@ async function parseMchpConfig(file2, projectRoot, scopeOverride) {
41806
41805
  }
41807
41806
  return result;
41808
41807
  }
41809
- function detectScope2(relativePath) {
41808
+ function detectScope(relativePath) {
41810
41809
  const normalized = relativePath.replace(/\\/g, "/");
41811
41810
  if (normalized.startsWith("~/")) return "global";
41812
41811
  if (normalized.endsWith(".mcph.local.json")) return "project-local";
@@ -42168,29 +42167,33 @@ async function checkPaths(file2, projectRoot) {
42168
42167
  function findClosestMatch(target, files) {
42169
42168
  const targetNorm = target.replace(/\\/g, "/");
42170
42169
  const targetBase = path3.basename(targetNorm);
42171
- let bestMatch = null;
42172
- let bestDistance = Infinity;
42170
+ let basenameMatch = null;
42171
+ let basenameDistance = Infinity;
42173
42172
  for (const file2 of files) {
42174
42173
  const fileNorm = file2.replace(/\\/g, "/");
42175
42174
  if (path3.basename(fileNorm) === targetBase && fileNorm !== targetNorm) {
42176
42175
  const dist = levenshtein(targetNorm, fileNorm);
42177
- if (dist < bestDistance) {
42178
- bestDistance = dist;
42179
- bestMatch = fileNorm;
42176
+ if (dist < basenameDistance) {
42177
+ basenameDistance = dist;
42178
+ basenameMatch = fileNorm;
42180
42179
  }
42181
42180
  }
42182
42181
  }
42183
- if (!bestMatch) {
42184
- for (const file2 of files) {
42185
- const fileNorm = file2.replace(/\\/g, "/");
42186
- const dist = levenshtein(targetNorm, fileNorm);
42187
- if (dist < bestDistance && dist <= Math.max(targetNorm.length * 0.4, 5)) {
42188
- bestDistance = dist;
42189
- bestMatch = fileNorm;
42190
- }
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;
42191
42194
  }
42192
42195
  }
42193
- return bestMatch;
42196
+ return fullPathMatch;
42194
42197
  }
42195
42198
  var import_fast_levenshtein, levenshtein, cachedProjectFiles;
42196
42199
  var init_paths = __esm({
@@ -42208,6 +42211,24 @@ var init_paths = __esm({
42208
42211
  // src/core/checks/commands.ts
42209
42212
  import * as fs4 from "node:fs";
42210
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
+ }
42211
42232
  async function checkCommands(file2, projectRoot) {
42212
42233
  const issues = [];
42213
42234
  const pkgJson = loadPackageJson(projectRoot);
@@ -42246,10 +42267,9 @@ async function checkCommands(file2, projectRoot) {
42246
42267
  }
42247
42268
  continue;
42248
42269
  }
42249
- const npxMatch = cmd.match(NPX_PATTERN);
42250
- if (npxMatch && pkgJson) {
42251
- const pkgName = npxMatch[1];
42252
- if (pkgName.startsWith("-")) continue;
42270
+ if (/^npx\b/.test(cmd) && pkgJson) {
42271
+ const pkgName = extractNpxPackage(cmd);
42272
+ if (!pkgName) continue;
42253
42273
  const allDeps = {
42254
42274
  ...pkgJson.dependencies,
42255
42275
  ...pkgJson.devDependencies,
@@ -42324,7 +42344,7 @@ async function checkCommands(file2, projectRoot) {
42324
42344
  }
42325
42345
  function loadMakefile(projectRoot) {
42326
42346
  try {
42327
- return fs4.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8");
42347
+ return stripBom(fs4.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8"));
42328
42348
  } catch {
42329
42349
  return null;
42330
42350
  }
@@ -42333,14 +42353,13 @@ function hasMakeTarget(makefile, target) {
42333
42353
  const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
42334
42354
  return pattern.test(makefile);
42335
42355
  }
42336
- var NPM_SCRIPT_PATTERN, MAKE_PATTERN, NPX_PATTERN;
42356
+ var NPM_SCRIPT_PATTERN, MAKE_PATTERN;
42337
42357
  var init_commands = __esm({
42338
42358
  "src/core/checks/commands.ts"() {
42339
42359
  "use strict";
42340
42360
  init_fs();
42341
42361
  NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
42342
42362
  MAKE_PATTERN = /^make\s+(\S+)/;
42343
- NPX_PATTERN = /^npx\s+(\S+)/;
42344
42363
  }
42345
42364
  });
42346
42365
 
@@ -42515,7 +42534,14 @@ function isAlwaysLoaded(file2) {
42515
42534
  return !hasPathsFrontmatter(file2.content);
42516
42535
  }
42517
42536
  const basename4 = rel.split("/").pop() ?? "";
42518
- 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;
42519
42545
  }
42520
42546
  function computeSectionCosts(file2) {
42521
42547
  if (file2.sections.length === 0) return [];
@@ -42537,7 +42563,7 @@ function loadSettingsSources(projectRoot) {
42537
42563
  for (const p2 of candidates) {
42538
42564
  let content;
42539
42565
  try {
42540
- content = fs5.readFileSync(p2, "utf-8");
42566
+ content = stripBom(fs5.readFileSync(p2, "utf-8"));
42541
42567
  } catch {
42542
42568
  continue;
42543
42569
  }
@@ -42553,9 +42579,17 @@ function canonicalizeCommand(backticked) {
42553
42579
  const beforeFlags = backticked.trim().split(/\s+--?/, 1)[0];
42554
42580
  return beforeFlags.replace(/\s+/g, " ");
42555
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
+ }
42556
42591
  function commandIsEnforced(cmd, settings) {
42557
- const escaped = cmd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42558
- const pattern = new RegExp(`\\b${escaped}\\b`, "i");
42592
+ const pattern = buildCommandPattern(cmd);
42559
42593
  for (const s of settings) {
42560
42594
  for (const entry of s.permissions?.deny ?? []) {
42561
42595
  if (pattern.test(entry)) return true;
@@ -42633,13 +42667,14 @@ function checkAggregateTierTokens(files) {
42633
42667
  suggestion: "Consider moving the largest files or their heaviest sections to on-demand tiers (skills, subagents, memory)."
42634
42668
  };
42635
42669
  }
42636
- var ALWAYS_LOADED_BASENAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
42670
+ var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
42637
42671
  var init_tier_tokens = __esm({
42638
42672
  "src/core/checks/tier-tokens.ts"() {
42639
42673
  "use strict";
42640
42674
  init_tokens();
42675
+ init_fs();
42641
42676
  init_tokens2();
42642
- ALWAYS_LOADED_BASENAMES = /* @__PURE__ */ new Set([
42677
+ ALWAYS_LOADED_NAMES = [
42643
42678
  "CLAUDE.md",
42644
42679
  "CLAUDE.local.md",
42645
42680
  "AGENTS.md",
@@ -42654,15 +42689,42 @@ var init_tier_tokens = __esm({
42654
42689
  ".rules",
42655
42690
  ".goosehints",
42656
42691
  "replit.md",
42657
- "copilot-instructions.md",
42658
- "guidelines.md",
42659
- "instructions.md"
42660
- ]);
42692
+ ".github/copilot-instructions.md",
42693
+ ".junie/guidelines.md",
42694
+ ".junie/AGENTS.md",
42695
+ ".goose/instructions.md"
42696
+ ];
42661
42697
  TOP_SECTIONS_TO_REPORT = 3;
42662
42698
  INVIOLABLE_WITH_COMMAND = /\b(NEVER|ALWAYS|DON'?T|DO NOT|MUST NOT)\b[^.!?`]{0,80}`([^`]+)`/i;
42663
42699
  }
42664
42700
  });
42665
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
+
42666
42728
  // src/core/checks/redundancy.ts
42667
42729
  import * as path7 from "node:path";
42668
42730
  function compilePatterns(allDeps) {
@@ -42685,6 +42747,20 @@ function compilePatterns(allDeps) {
42685
42747
  }
42686
42748
  return compiled;
42687
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
+ }
42688
42764
  async function checkRedundancy(file2, projectRoot) {
42689
42765
  const issues = [];
42690
42766
  const pkgJson = loadPackageJson(projectRoot);
@@ -42695,7 +42771,7 @@ async function checkRedundancy(file2, projectRoot) {
42695
42771
  ...Object.keys(pkgJson.peerDependencies || {}),
42696
42772
  ...Object.keys(pkgJson.optionalDependencies || {})
42697
42773
  ]);
42698
- const compiledPatterns = compilePatterns(allDeps);
42774
+ const compiledPatterns = getCompiledPatterns(projectRoot, allDeps);
42699
42775
  const lines2 = file2.content.split("\n");
42700
42776
  for (let i2 = 0; i2 < lines2.length; i2++) {
42701
42777
  const line = lines2[i2];
@@ -42746,10 +42822,14 @@ async function checkRedundancy(file2, projectRoot) {
42746
42822
  }
42747
42823
  function checkDuplicateContent(files) {
42748
42824
  const issues = [];
42749
- const DUPLICATE_CONTENT_THRESHOLD = 0.6;
42825
+ const lineSets = files.map((f) => toLineSet(f.content, DUPLICATE_CONTENT_MIN_TOKEN_LEN));
42750
42826
  for (let i2 = 0; i2 < files.length; i2++) {
42827
+ const a = lineSets[i2];
42828
+ if (a.size === 0) continue;
42751
42829
  for (let j3 = i2 + 1; j3 < files.length; j3++) {
42752
- 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);
42753
42833
  if (overlap >= DUPLICATE_CONTENT_THRESHOLD) {
42754
42834
  issues.push({
42755
42835
  severity: "warning",
@@ -42764,29 +42844,15 @@ function checkDuplicateContent(files) {
42764
42844
  }
42765
42845
  return issues;
42766
42846
  }
42767
- function calculateLineOverlap(contentA, contentB) {
42768
- const linesA = new Set(
42769
- contentA.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
42770
- );
42771
- const linesB = new Set(
42772
- contentB.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
42773
- );
42774
- if (linesA.size === 0 || linesB.size === 0) return 0;
42775
- let intersection2 = 0;
42776
- for (const line of linesA) {
42777
- if (linesB.has(line)) intersection2++;
42778
- }
42779
- const unionSize = linesA.size + linesB.size - intersection2;
42780
- return intersection2 / unionSize;
42781
- }
42782
42847
  function escapeRegex2(str) {
42783
42848
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42784
42849
  }
42785
- var PACKAGE_TECH_MAP;
42850
+ var PACKAGE_TECH_MAP, compiledPatternsCache, DUPLICATE_CONTENT_THRESHOLD, DUPLICATE_CONTENT_MIN_TOKEN_LEN;
42786
42851
  var init_redundancy = __esm({
42787
42852
  "src/core/checks/redundancy.ts"() {
42788
42853
  "use strict";
42789
42854
  init_fs();
42855
+ init_similarity();
42790
42856
  init_tokens();
42791
42857
  PACKAGE_TECH_MAP = {
42792
42858
  react: ["React", "react"],
@@ -42839,6 +42905,9 @@ var init_redundancy = __esm({
42839
42905
  cypress: ["Cypress"],
42840
42906
  puppeteer: ["Puppeteer"]
42841
42907
  };
42908
+ compiledPatternsCache = /* @__PURE__ */ new Map();
42909
+ DUPLICATE_CONTENT_THRESHOLD = 0.6;
42910
+ DUPLICATE_CONTENT_MIN_TOKEN_LEN = 10;
42842
42911
  }
42843
42912
  });
42844
42913
 
@@ -43156,7 +43225,7 @@ var init_contradictions = __esm({
43156
43225
  function parseFrontmatter(content) {
43157
43226
  const lines = content.split("\n");
43158
43227
  if (lines[0]?.trim() !== "---") {
43159
- return { found: false, fields: {}, endLine: 0 };
43228
+ return { found: false, fields: {}, endLine: 0, unclosed: false };
43160
43229
  }
43161
43230
  const fields = {};
43162
43231
  let endLine = 0;
@@ -43183,9 +43252,9 @@ function parseFrontmatter(content) {
43183
43252
  }
43184
43253
  }
43185
43254
  if (endLine === 0) {
43186
- return { found: true, fields, endLine: lines.length };
43255
+ return { found: true, fields, endLine: lines.length, unclosed: true };
43187
43256
  }
43188
- return { found: true, fields, endLine };
43257
+ return { found: true, fields, endLine, unclosed: false };
43189
43258
  }
43190
43259
  function isCursorMdc(file2) {
43191
43260
  return file2.relativePath.endsWith(".mdc");
@@ -43196,6 +43265,23 @@ function isCopilotInstructions(file2) {
43196
43265
  function isWindsurfRule(file2) {
43197
43266
  return file2.relativePath.includes(".windsurf/rules/") && file2.relativePath.endsWith(".md");
43198
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
+ }
43199
43285
  async function checkFrontmatter(file2, _projectRoot) {
43200
43286
  const issues = [];
43201
43287
  if (isCursorMdc(file2)) {
@@ -43207,9 +43293,23 @@ async function checkFrontmatter(file2, _projectRoot) {
43207
43293
  }
43208
43294
  return issues;
43209
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
+ }
43210
43306
  function validateCursorMdc(file2) {
43211
43307
  const issues = [];
43212
43308
  const fm = parseFrontmatter(file2.content);
43309
+ if (fm.unclosed) {
43310
+ issues.push(unclosedFrontmatterIssue());
43311
+ return issues;
43312
+ }
43213
43313
  if (!fm.found) {
43214
43314
  issues.push({
43215
43315
  severity: "warning",
@@ -43256,13 +43356,13 @@ function validateCursorMdc(file2) {
43256
43356
  }
43257
43357
  if ("globs" in fm.fields) {
43258
43358
  const val = fm.fields["globs"];
43259
- if (val && !val.startsWith("[") && !val.startsWith('"') && !val.includes("*") && !val.includes("/")) {
43359
+ if (val && hasUnbalancedBracketsOrQuotes(val)) {
43260
43360
  issues.push({
43261
43361
  severity: "warning",
43262
43362
  check: "frontmatter",
43263
43363
  ruleId: "frontmatter/invalid-value",
43264
43364
  line: 1,
43265
- message: `Possibly invalid globs value: "${val}"`,
43365
+ message: `Possibly malformed globs value: "${val}"`,
43266
43366
  suggestion: 'globs should be a glob pattern like "src/**/*.ts" or an array like ["*.ts", "*.tsx"]'
43267
43367
  });
43268
43368
  }
@@ -43272,6 +43372,10 @@ function validateCursorMdc(file2) {
43272
43372
  function validateCopilotInstructions(file2) {
43273
43373
  const issues = [];
43274
43374
  const fm = parseFrontmatter(file2.content);
43375
+ if (fm.unclosed) {
43376
+ issues.push(unclosedFrontmatterIssue());
43377
+ return issues;
43378
+ }
43275
43379
  if (!fm.found) {
43276
43380
  issues.push({
43277
43381
  severity: "info",
@@ -43298,6 +43402,10 @@ function validateCopilotInstructions(file2) {
43298
43402
  function validateWindsurfRule(file2) {
43299
43403
  const issues = [];
43300
43404
  const fm = parseFrontmatter(file2.content);
43405
+ if (fm.unclosed) {
43406
+ issues.push(unclosedFrontmatterIssue());
43407
+ return issues;
43408
+ }
43301
43409
  if (!fm.found) {
43302
43410
  issues.push({
43303
43411
  severity: "info",
@@ -43469,6 +43577,10 @@ function isEnvVarRef(value) {
43469
43577
  function isKnownApiKey(value) {
43470
43578
  return API_KEY_PATTERNS.some((p2) => p2.test(value));
43471
43579
  }
43580
+ function nameSuggestsSecret(name) {
43581
+ const upper = name.toUpperCase();
43582
+ return SECRET_NAME_KEYWORDS.some((kw) => upper.includes(kw));
43583
+ }
43472
43584
  function isHighEntropySecret(value) {
43473
43585
  if (isEnvVarRef(value)) return false;
43474
43586
  return HIGH_ENTROPY_PATTERN.test(value);
@@ -43517,8 +43629,9 @@ async function checkMcpSecurity(config2, _projectRoot) {
43517
43629
  }
43518
43630
  }
43519
43631
  if (server2.env) {
43520
- for (const envValue of Object.values(server2.env)) {
43521
- 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) {
43522
43635
  const envVar = deriveEnvVarName(server2.name, "API_KEY");
43523
43636
  issues.push({
43524
43637
  severity: "error",
@@ -43565,7 +43678,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43565
43678
  }
43566
43679
  return issues;
43567
43680
  }
43568
- 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;
43569
43682
  var init_security = __esm({
43570
43683
  "src/core/checks/mcp/security.ts"() {
43571
43684
  "use strict";
@@ -43594,6 +43707,22 @@ var init_security = __esm({
43594
43707
  ENV_VAR_REF = /\$\{[^}]+\}/;
43595
43708
  HIGH_ENTROPY_PATTERN = /^[A-Za-z0-9+/=_-]{21,}$/;
43596
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
+ ];
43597
43726
  }
43598
43727
  });
43599
43728
 
@@ -44360,6 +44489,12 @@ function extractPaths(content) {
44360
44489
  paths.push(p2);
44361
44490
  }
44362
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
+ }
44363
44498
  return [...new Set(paths)];
44364
44499
  }
44365
44500
  function parseFrontmatter2(content) {
@@ -44389,7 +44524,7 @@ function parseFrontmatter2(content) {
44389
44524
  };
44390
44525
  }
44391
44526
  async function parseMemoryFile(filePath, projectDir) {
44392
- const content = await readFile(filePath, "utf-8");
44527
+ const content = stripBom(await readFile(filePath, "utf-8"));
44393
44528
  const { name, description, type, body } = parseFrontmatter2(content);
44394
44529
  const referencedPaths = extractPaths(body);
44395
44530
  return {
@@ -44402,11 +44537,13 @@ async function parseMemoryFile(filePath, projectDir) {
44402
44537
  referencedPaths
44403
44538
  };
44404
44539
  }
44405
- var PATH_PATTERN2;
44540
+ var PATH_PATTERN2, BARE_FILE_PATH;
44406
44541
  var init_session_parser = __esm({
44407
44542
  "src/core/session-parser.ts"() {
44408
44543
  "use strict";
44544
+ init_fs();
44409
44545
  PATH_PATTERN2 = /(?:^|\s|['"`(])([.~/][^\s'"`),;:!?]+)/g;
44546
+ BARE_FILE_PATH = /(?:^|[\s`"'(])([\w][\w-]*(?:\/[\w.-]+)+\.[a-zA-Z0-9]{1,8})\b/g;
44410
44547
  }
44411
44548
  });
44412
44549
 
@@ -44438,30 +44575,46 @@ async function parseJsonlFiltered(filePath, filter) {
44438
44575
  }
44439
44576
  return results;
44440
44577
  }
44441
- async function readClaudeHistory() {
44442
- const historyPath = join5(home, ".claude", "history.jsonl");
44443
- return parseJsonlFiltered(historyPath, (entry) => {
44444
- 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;
44445
44593
  return {
44446
- display: entry.display,
44447
- timestamp: entry.timestamp || 0,
44448
- project: entry.project.replace(/\\/g, "/"),
44449
- sessionId: entry.sessionId || "",
44450
- 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
44451
44599
  };
44452
44600
  });
44453
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
+ }
44454
44611
  async function readCodexHistory() {
44455
- const historyPath = join5(home, ".codex", "history.jsonl");
44456
- return parseJsonlFiltered(historyPath, (entry) => {
44457
- if (!entry.display && !entry.command) return null;
44458
- return {
44459
- display: entry.display || entry.command || "",
44460
- timestamp: entry.timestamp || 0,
44461
- project: (entry.project || entry.cwd || "").replace(/\\/g, "/"),
44462
- sessionId: entry.sessionId || "",
44463
- provider: "codex-cli"
44464
- };
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
44465
44618
  });
44466
44619
  }
44467
44620
  async function readClaudeMemories() {
@@ -44526,47 +44679,29 @@ async function detectSiblings(projectRoot) {
44526
44679
  if (orgMatch) currentOrg = orgMatch[1];
44527
44680
  } catch {
44528
44681
  }
44529
- const gitCandidates = candidates.filter((c3) => existsSync(join5(c3.fullPath, ".git")));
44530
- const results = await Promise.all(
44531
- gitCandidates.map(async (c3) => {
44532
- const sibling = { path: c3.entryPath.replace(/\\/g, "/"), name: c3.name };
44533
- try {
44534
- const git = simpleGit(c3.fullPath);
44535
- const remotes = await git.getRemotes(true);
44536
- const origin = remotes.find((r2) => r2.name === "origin");
44537
- if (origin?.refs?.fetch) {
44538
- sibling.gitRemoteUrl = origin.refs.fetch;
44539
- const orgMatch = origin.refs.fetch.match(/github\.com[:/]([^/]+)\//);
44540
- if (orgMatch) sibling.gitOrg = orgMatch[1];
44541
- }
44542
- } catch {
44543
- }
44544
- return sibling;
44545
- })
44546
- );
44682
+ const results = await Promise.all(candidates.map((c3) => resolveSibling(c3)));
44547
44683
  if (currentOrg) {
44548
- return results.filter((s) => s.gitOrg === currentOrg);
44684
+ return results.filter((s) => !s.gitOrg || s.gitOrg === currentOrg);
44549
44685
  }
44550
44686
  return results;
44551
44687
  }
44552
- const siblings = await Promise.all(
44553
- candidates.map(async (c3) => {
44554
- const sibling = { path: c3.entryPath.replace(/\\/g, "/"), name: c3.name };
44555
- try {
44556
- const git = simpleGit(c3.fullPath);
44557
- const remotes = await git.getRemotes(true);
44558
- const origin = remotes.find((r2) => r2.name === "origin");
44559
- if (origin?.refs?.fetch) {
44560
- sibling.gitRemoteUrl = origin.refs.fetch;
44561
- const orgMatch = origin.refs.fetch.match(/github\.com[:/]([^/]+)\//);
44562
- if (orgMatch) sibling.gitOrg = orgMatch[1];
44563
- }
44564
- } catch {
44565
- }
44566
- return sibling;
44567
- })
44568
- );
44569
- 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;
44570
44705
  }
44571
44706
  async function scanSessionData(projectRoot) {
44572
44707
  const providers = detectProviders();
@@ -44686,48 +44821,57 @@ var init_missing_secret = __esm({
44686
44821
  });
44687
44822
 
44688
44823
  // src/core/checks/session/diverged-file.ts
44689
- import { readFile as readFile2 } from "node:fs/promises";
44824
+ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
44690
44825
  import { join as join6 } from "node:path";
44691
44826
  import { existsSync as existsSync2 } from "node:fs";
44692
- function calculateOverlap(a, b2) {
44693
- const linesA = new Set(
44694
- a.split("\n").map((l) => l.trim()).filter((l) => l.length > 3)
44695
- );
44696
- const linesB = new Set(
44697
- b2.split("\n").map((l) => l.trim()).filter((l) => l.length > 3)
44698
- );
44699
- if (linesA.size === 0 && linesB.size === 0) return 1;
44700
- if (linesA.size === 0 || linesB.size === 0) return 0;
44701
- let intersection2 = 0;
44702
- for (const line of linesA) {
44703
- 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;
44704
44836
  }
44705
- const unionSize = linesA.size + linesB.size - intersection2;
44706
- 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;
44707
44856
  }
44708
44857
  async function checkDivergedFile(ctx) {
44709
44858
  const issues = [];
44710
44859
  for (const fileName of CANONICAL_FILES) {
44711
44860
  const currentPath = join6(ctx.currentProject, fileName);
44712
44861
  if (!existsSync2(currentPath)) continue;
44713
- let currentContent;
44714
- try {
44715
- currentContent = await readFile2(currentPath, "utf-8");
44716
- } catch {
44717
- continue;
44718
- }
44862
+ const currentLineSet = await loadLineSet(currentPath);
44863
+ if (!currentLineSet) continue;
44719
44864
  const diverged = [];
44720
44865
  for (const sib of ctx.siblings) {
44721
44866
  const sibPath = join6(sib.path, fileName);
44722
44867
  if (!existsSync2(sibPath)) continue;
44723
- try {
44724
- const sibContent = await readFile2(sibPath, "utf-8");
44725
- const overlap = calculateOverlap(currentContent, sibContent);
44726
- if (overlap >= 0.2 && overlap < 0.9) {
44727
- diverged.push({ sibling: sib.name, overlap: Math.round(overlap * 100) });
44728
- }
44729
- } catch {
44730
- 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) });
44731
44875
  }
44732
44876
  }
44733
44877
  if (diverged.length > 0) {
@@ -44745,10 +44889,12 @@ async function checkDivergedFile(ctx) {
44745
44889
  }
44746
44890
  return issues;
44747
44891
  }
44748
- var CANONICAL_FILES;
44892
+ var CANONICAL_FILES, MIN_TOKEN_LEN, CACHE_MAX_ENTRIES, lineSetCache;
44749
44893
  var init_diverged_file = __esm({
44750
44894
  "src/core/checks/session/diverged-file.ts"() {
44751
44895
  "use strict";
44896
+ init_similarity();
44897
+ init_fs();
44752
44898
  CANONICAL_FILES = [
44753
44899
  "release.sh",
44754
44900
  ".github/workflows/ci.yml",
@@ -44759,6 +44905,9 @@ var init_diverged_file = __esm({
44759
44905
  "tsconfig.json",
44760
44906
  ".gitignore"
44761
44907
  ];
44908
+ MIN_TOKEN_LEN = 3;
44909
+ CACHE_MAX_ENTRIES = 256;
44910
+ lineSetCache = /* @__PURE__ */ new Map();
44762
44911
  }
44763
44912
  });
44764
44913
 
@@ -44866,24 +45015,10 @@ var init_stale_memory = __esm({
44866
45015
  });
44867
45016
 
44868
45017
  // src/core/checks/session/duplicate-memory.ts
44869
- function calculateLineOverlap2(a, b2) {
44870
- const linesA = new Set(
44871
- a.split("\n").map((l) => l.trim()).filter((l) => l.length > 5)
44872
- );
44873
- const linesB = new Set(
44874
- b2.split("\n").map((l) => l.trim()).filter((l) => l.length > 5)
44875
- );
44876
- if (linesA.size === 0 || linesB.size === 0) return 0;
44877
- let intersection2 = 0;
44878
- for (const line of linesA) {
44879
- if (linesB.has(line)) intersection2++;
44880
- }
44881
- const unionSize = linesA.size + linesB.size - intersection2;
44882
- return intersection2 / unionSize;
44883
- }
44884
45018
  async function checkDuplicateMemory(ctx) {
44885
45019
  const issues = [];
44886
45020
  const reported = /* @__PURE__ */ new Set();
45021
+ const lineSets = ctx.memories.map((m) => toLineSet(m.content, MIN_TOKEN_LEN2));
44887
45022
  for (let i2 = 0; i2 < ctx.memories.length; i2++) {
44888
45023
  for (let j3 = i2 + 1; j3 < ctx.memories.length; j3++) {
44889
45024
  const a = ctx.memories[i2];
@@ -44893,7 +45028,7 @@ async function checkDuplicateMemory(ctx) {
44893
45028
  const bIsCurrent = projectDirMatchesPath(b2.projectDir, ctx.currentProject);
44894
45029
  if (!aIsCurrent && !bIsCurrent) continue;
44895
45030
  if (a.content.length < 50 || b2.content.length < 50) continue;
44896
- const overlap = calculateLineOverlap2(a.content, b2.content);
45031
+ const overlap = jaccardSimilarityFromSets(lineSets[i2], lineSets[j3]);
44897
45032
  if (overlap < 0.6) continue;
44898
45033
  const pairKey = [a.filePath, b2.filePath].sort().join("::");
44899
45034
  if (reported.has(pairKey)) continue;
@@ -44915,10 +45050,13 @@ async function checkDuplicateMemory(ctx) {
44915
45050
  }
44916
45051
  return issues;
44917
45052
  }
45053
+ var MIN_TOKEN_LEN2;
44918
45054
  var init_duplicate_memory = __esm({
44919
45055
  "src/core/checks/session/duplicate-memory.ts"() {
44920
45056
  "use strict";
44921
45057
  init_session_parser();
45058
+ init_similarity();
45059
+ MIN_TOKEN_LEN2 = 5;
44922
45060
  }
44923
45061
  });
44924
45062
 
@@ -45013,24 +45151,23 @@ var init_loop_detection = __esm({
45013
45151
  });
45014
45152
 
45015
45153
  // src/core/checks/session/memory-index-overflow.ts
45016
- import { readFile as readFile3, stat as stat2 } from "node:fs/promises";
45154
+ import { readFile as readFile3 } from "node:fs/promises";
45017
45155
  import { join as join8 } from "node:path";
45018
45156
  async function checkMemoryIndexOverflow(ctx) {
45019
45157
  const home2 = process.env.HOME || process.env.USERPROFILE || "";
45020
45158
  if (!home2) return [];
45021
45159
  const encoded = encodeProjectDir(ctx.currentProject);
45022
45160
  const memoryFile = join8(home2, ".claude", "projects", encoded, "memory", "MEMORY.md");
45023
- let stats;
45161
+ let content;
45024
45162
  try {
45025
- stats = await stat2(memoryFile);
45163
+ content = stripBom(await readFile3(memoryFile, "utf-8"));
45026
45164
  } catch {
45027
45165
  return [];
45028
45166
  }
45029
- const content = await readFile3(memoryFile, "utf-8").catch(() => "");
45030
45167
  if (!content) return [];
45031
45168
  const lines = content.split("\n");
45032
45169
  const lineCount = lines.length;
45033
- const byteSize = stats.size;
45170
+ const byteSize = Buffer.byteLength(content, "utf8");
45034
45171
  const issues = [];
45035
45172
  if (lineCount > MAX_LINES) {
45036
45173
  const excess = lineCount - MAX_LINES;
@@ -45063,6 +45200,7 @@ var init_memory_index_overflow = __esm({
45063
45200
  "src/core/checks/session/memory-index-overflow.ts"() {
45064
45201
  "use strict";
45065
45202
  init_session_parser();
45203
+ init_fs();
45066
45204
  MAX_LINES = 200;
45067
45205
  MAX_BYTES = 25 * 1024;
45068
45206
  }
@@ -45089,7 +45227,7 @@ async function findReleaseWorkflows(projectRoot) {
45089
45227
  continue;
45090
45228
  }
45091
45229
  try {
45092
- const content = await readFile4(join9(workflowDir, f), "utf-8");
45230
+ const content = stripBom(await readFile4(join9(workflowDir, f), "utf-8"));
45093
45231
  const nameMatch = content.match(/^name:\s*(.+)$/m);
45094
45232
  if (nameMatch && RELEASE_FILENAME_PATTERNS.some((p2) => p2.test(nameMatch[1]))) {
45095
45233
  releaseWorkflows.push(f);
@@ -45126,6 +45264,7 @@ var RELEASE_FILENAME_PATTERNS, RELEASE_DOC_PATTERNS;
45126
45264
  var init_ci_coverage = __esm({
45127
45265
  "src/core/checks/ci-coverage.ts"() {
45128
45266
  "use strict";
45267
+ init_fs();
45129
45268
  RELEASE_FILENAME_PATTERNS = [/release/i, /deploy/i, /publish/i, /\bcd\b/i];
45130
45269
  RELEASE_DOC_PATTERNS = [
45131
45270
  /release\s+(process|workflow|steps|via|by|using)/i,
@@ -45160,7 +45299,7 @@ async function findSecretUsages(projectRoot) {
45160
45299
  if (!(f.endsWith(".yml") || f.endsWith(".yaml"))) continue;
45161
45300
  let content;
45162
45301
  try {
45163
- content = await readFile5(join10(workflowDir, f), "utf-8");
45302
+ content = stripBom(await readFile5(join10(workflowDir, f), "utf-8"));
45164
45303
  } catch {
45165
45304
  continue;
45166
45305
  }
@@ -45177,7 +45316,7 @@ async function findSecretUsages(projectRoot) {
45177
45316
  }
45178
45317
  function contextMentionsSecret(files, secretName) {
45179
45318
  const escaped = secretName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
45180
- const pattern = new RegExp(`\\b${escaped.replace(/_/g, "[_\\s-]")}\\b`, "i");
45319
+ const pattern = new RegExp(`\\b${escaped.replace(/_/g, "[ _-]")}\\b`, "i");
45181
45320
  return files.some((f) => pattern.test(f.content));
45182
45321
  }
45183
45322
  async function checkCiSecrets(files, projectRoot) {
@@ -45201,6 +45340,7 @@ var BUILTIN_SECRETS, SECRETS_REGEX;
45201
45340
  var init_ci_secrets = __esm({
45202
45341
  "src/core/checks/ci-secrets.ts"() {
45203
45342
  "use strict";
45343
+ init_fs();
45204
45344
  BUILTIN_SECRETS = /* @__PURE__ */ new Set([
45205
45345
  "GITHUB_TOKEN",
45206
45346
  "ACTIONS_RUNTIME_TOKEN",
@@ -45221,7 +45361,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
45221
45361
  import { resolve as resolve8, dirname as dirname3 } from "node:path";
45222
45362
  import { fileURLToPath } from "node:url";
45223
45363
  function loadVersion() {
45224
- if (true) return "0.9.19";
45364
+ if (true) return "0.9.20";
45225
45365
  const __dir = dirname3(fileURLToPath(import.meta.url));
45226
45366
  const pkgPath = resolve8(__dir, "../package.json");
45227
45367
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
@@ -45245,6 +45385,11 @@ function hasMcphChecks(checks) {
45245
45385
  function hasSessionChecks(checks) {
45246
45386
  return checks.some((c3) => c3.startsWith("session-"));
45247
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
+ }
45248
45393
  async function runAudit(projectRoot, activeChecks, options = {}) {
45249
45394
  const fileResults = [];
45250
45395
  const shouldRunContextChecks = !options.mcpOnly && !options.mcphOnly && !options.sessionOnly;
@@ -45325,8 +45470,12 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45325
45470
  );
45326
45471
  mcpConfigs.push(...globalConfigs);
45327
45472
  }
45328
- const activeMcpChecks = activeChecks.filter((c3) => c3.startsWith("mcp-"));
45329
- 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
+ );
45330
45479
  for (const config2 of mcpConfigs) {
45331
45480
  const checkPromises = [];
45332
45481
  if (mcpChecksToRun.includes("mcp-schema"))
@@ -45372,7 +45521,7 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45372
45521
  const projectMcphFiles = await scanForMcphConfigs(projectRoot);
45373
45522
  const mcphConfigs = await Promise.all(
45374
45523
  projectMcphFiles.map(
45375
- (f) => parseMchpConfig(
45524
+ (f) => parseMcphConfig(
45376
45525
  f,
45377
45526
  projectRoot,
45378
45527
  f.relativePath.endsWith(".mcph.local.json") ? "project-local" : "project"
@@ -45382,12 +45531,16 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45382
45531
  if (options.mcphGlobal) {
45383
45532
  const globalMcphFiles = await scanGlobalMcphConfigs();
45384
45533
  const globalMcphConfigs = await Promise.all(
45385
- globalMcphFiles.map((f) => parseMchpConfig(f, projectRoot, "global"))
45534
+ globalMcphFiles.map((f) => parseMcphConfig(f, projectRoot, "global"))
45386
45535
  );
45387
45536
  mcphConfigs.push(...globalMcphConfigs);
45388
45537
  }
45389
- const activeMcphChecks = activeChecks.filter((c3) => c3.startsWith("mcph-"));
45390
- 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
+ );
45391
45544
  for (const config2 of mcphConfigs) {
45392
45545
  const checkPromises = [];
45393
45546
  if (mcphChecksToRun.includes("mcph-token-security")) {
@@ -45411,14 +45564,16 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45411
45564
  }
45412
45565
  const results = await Promise.all(checkPromises);
45413
45566
  const issues = results.flat();
45414
- for (const err of config2.parseErrors) {
45415
- issues.push({
45416
- severity: "error",
45417
- check: "mcph-schema-conformance",
45418
- ruleId: "mcph-config/parse-error",
45419
- line: 1,
45420
- message: err
45421
- });
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
+ }
45422
45577
  }
45423
45578
  const lines = config2.content.split("\n").length;
45424
45579
  fileResults.push({
@@ -45454,15 +45609,13 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45454
45609
  sessionPromises.push(checkMemoryIndexOverflow(sessionCtx));
45455
45610
  const sessionResults = await Promise.all(sessionPromises);
45456
45611
  const sessionIssues = sessionResults.flat();
45457
- if (sessionIssues.length > 0) {
45458
- fileResults.push({
45459
- path: "~/.claude/ (session audit)",
45460
- isSymlink: false,
45461
- tokens: 0,
45462
- lines: 0,
45463
- issues: sessionIssues
45464
- });
45465
- }
45612
+ fileResults.push({
45613
+ path: "~/.claude/ (session audit)",
45614
+ isSymlink: false,
45615
+ tokens: 0,
45616
+ lines: 0,
45617
+ issues: sessionIssues
45618
+ });
45466
45619
  }
45467
45620
  }
45468
45621
  let estimatedWaste = 0;
@@ -46219,11 +46372,22 @@ var init_fixer = __esm({
46219
46372
 
46220
46373
  // src/mcp/server.ts
46221
46374
  var server_exports = {};
46375
+ __export(server_exports, {
46376
+ server: () => server,
46377
+ startServer: () => startServer
46378
+ });
46222
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
+ }
46223
46385
  function validateProjectPath(rawPath) {
46224
46386
  if (!rawPath) return process.cwd();
46225
46387
  if (PATH_DISALLOWED.test(rawPath)) {
46226
- 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
+ );
46227
46391
  }
46228
46392
  const resolved = path10.resolve(rawPath);
46229
46393
  if (!isDirectory(resolved)) {
@@ -46233,12 +46397,28 @@ function validateProjectPath(rawPath) {
46233
46397
  }
46234
46398
  function validateFilePathInput(rawPath) {
46235
46399
  if (PATH_DISALLOWED.test(rawPath)) {
46236
- 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
+ );
46237
46403
  }
46238
46404
  }
46239
- var contextCheckEnum, mcpCheckEnum, mcphCheckEnum, 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;
46240
46420
  var init_server3 = __esm({
46241
- async "src/mcp/server.ts"() {
46421
+ "src/mcp/server.ts"() {
46242
46422
  "use strict";
46243
46423
  init_mcp();
46244
46424
  init_stdio2();
@@ -46312,7 +46492,7 @@ var init_server3 = __esm({
46312
46492
  try {
46313
46493
  validateFilePathInput(filePath);
46314
46494
  const root = validateProjectPath(projectPath);
46315
- const resolved = path10.resolve(root, filePath);
46495
+ const resolved = resolveWithinRoot(filePath, root);
46316
46496
  const result = {
46317
46497
  path: filePath,
46318
46498
  exists: fileExists(resolved) || isDirectory(resolved)
@@ -46555,9 +46735,6 @@ var init_server3 = __esm({
46555
46735
  }
46556
46736
  }
46557
46737
  );
46558
- keepEncoderAlive(true);
46559
- transport = new StdioServerTransport();
46560
- await server.connect(transport);
46561
46738
  }
46562
46739
  });
46563
46740
 
@@ -53035,7 +53212,9 @@ var init_ora = __esm({
53035
53212
 
53036
53213
  // src/core/reporter.ts
53037
53214
  function classifyFile(f) {
53215
+ if (f.path === "(project)") return "context";
53038
53216
  if (f.path === "(mcp)") return "mcp";
53217
+ if (f.path === "(mcph)") return "mcph";
53039
53218
  if (f.path.includes("session audit")) return "session";
53040
53219
  for (const issue2 of f.issues) {
53041
53220
  if (issue2.check.startsWith("session-")) return "session";
@@ -53532,7 +53711,7 @@ function loadConfig(projectRoot) {
53532
53711
  const filePath = path11.join(projectRoot, filename);
53533
53712
  let content;
53534
53713
  try {
53535
- content = fs8.readFileSync(filePath, "utf-8");
53714
+ content = stripBom(fs8.readFileSync(filePath, "utf-8"));
53536
53715
  } catch {
53537
53716
  continue;
53538
53717
  }
@@ -53543,7 +53722,7 @@ function loadConfig(projectRoot) {
53543
53722
  function loadConfigFromExplicitPath(configPath) {
53544
53723
  let content;
53545
53724
  try {
53546
- content = fs8.readFileSync(configPath, "utf-8");
53725
+ content = stripBom(fs8.readFileSync(configPath, "utf-8"));
53547
53726
  } catch (err) {
53548
53727
  const detail = err instanceof Error ? err.message : String(err);
53549
53728
  throw new Error(`could not load config from ${configPath}: ${detail}`, { cause: err });
@@ -53555,6 +53734,7 @@ var init_config2 = __esm({
53555
53734
  "src/core/config.ts"() {
53556
53735
  "use strict";
53557
53736
  import_fast_levenshtein2 = __toESM(require_levenshtein(), 1);
53737
+ init_fs();
53558
53738
  levenshtein2 = import_fast_levenshtein2.default.get;
53559
53739
  KNOWN_CONFIG_KEYS = [
53560
53740
  "checks",
@@ -53999,7 +54179,8 @@ var init_cli = __esm({
53999
54179
  // src/index.ts
54000
54180
  var args = process.argv.slice(2);
54001
54181
  if (args[0] === "serve" || args.includes("--mcp-server")) {
54002
- await init_server3().then(() => server_exports);
54182
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server3(), server_exports));
54183
+ await startServer2();
54003
54184
  } else {
54004
54185
  const { runCli: runCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
54005
54186
  await runCli2();