@yawlabs/ctxlint 0.9.19 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +99 -57
  2. package/dist/index.js +776 -353
  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") {
@@ -34449,6 +34452,8 @@ async function scanGlobalMcpConfigs() {
34449
34452
  } catch {
34450
34453
  continue;
34451
34454
  }
34455
+ const isGeneralClaudeFile = normalized.endsWith(`${path2.sep}.claude.json`) || normalized.endsWith(`${path2.sep}.claude${path2.sep}settings.json`);
34456
+ if (isGeneralClaudeFile && !mcpFileHasMcpKey(normalized)) continue;
34452
34457
  const symlink = isSymlink(normalized);
34453
34458
  const target = symlink ? readSymlinkTarget(normalized) : void 0;
34454
34459
  found.push({
@@ -34461,6 +34466,14 @@ async function scanGlobalMcpConfigs() {
34461
34466
  }
34462
34467
  return found.sort((a, b2) => a.relativePath.localeCompare(b2.relativePath));
34463
34468
  }
34469
+ function mcpFileHasMcpKey(filePath) {
34470
+ try {
34471
+ const content = fs3.readFileSync(filePath, "utf8");
34472
+ return /"(mcpServers|servers)"\s*:/.test(content);
34473
+ } catch {
34474
+ return false;
34475
+ }
34476
+ }
34464
34477
  var CONTEXT_FILE_PATTERNS, IGNORED_DIRS2, MCP_CONFIG_PATTERNS, MCPH_CONFIG_PATTERNS;
34465
34478
  var init_scanner = __esm({
34466
34479
  "src/core/scanner.ts"() {
@@ -40656,10 +40669,9 @@ var init_git = __esm({
40656
40669
  });
40657
40670
 
40658
40671
  // src/core/mcp-parser.ts
40659
- async function parseMcpConfig(file2, projectRoot, scopeOverride) {
40672
+ async function parseMcpConfig(file2, projectRoot, scope) {
40660
40673
  const content = readFileContent(file2.absolutePath);
40661
40674
  const client = detectClient(file2.relativePath);
40662
- const scope = scopeOverride ?? detectScope(file2.relativePath);
40663
40675
  const expectedRootKey = client === "vscode" ? "servers" : "mcpServers";
40664
40676
  const isGitTracked = await checkGitTracked(file2.absolutePath, projectRoot);
40665
40677
  const result = {
@@ -40697,16 +40709,17 @@ async function parseMcpConfig(file2, projectRoot, scopeOverride) {
40697
40709
  return result;
40698
40710
  }
40699
40711
  const lines = content.split("\n");
40712
+ const rootKeyLine = findRootKeyLine(lines, rootKey);
40700
40713
  for (const [name, value] of Object.entries(serversObj)) {
40701
40714
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
40702
40715
  continue;
40703
40716
  }
40704
40717
  const raw = value;
40705
- const line = findServerLine(lines, name);
40706
- const transport2 = inferTransport(raw);
40718
+ const line = findServerLine(lines, name, rootKeyLine);
40719
+ const transport = inferTransport(raw);
40707
40720
  const entry = {
40708
40721
  name,
40709
- transport: transport2,
40722
+ transport,
40710
40723
  line,
40711
40724
  raw
40712
40725
  };
@@ -40718,7 +40731,7 @@ async function parseMcpConfig(file2, projectRoot, scopeOverride) {
40718
40731
  if (typeof raw.disabled === "boolean") entry.disabled = raw.disabled;
40719
40732
  if (Array.isArray(raw.autoApprove)) entry.autoApprove = raw.autoApprove.map(String);
40720
40733
  if (typeof raw.timeout === "number") entry.timeout = raw.timeout;
40721
- if (typeof raw.oauth === "object" && raw.oauth !== null)
40734
+ if (typeof raw.oauth === "object" && raw.oauth !== null && !Array.isArray(raw.oauth))
40722
40735
  entry.oauth = raw.oauth;
40723
40736
  if (typeof raw.headersHelper === "string") entry.headersHelper = raw.headersHelper;
40724
40737
  result.servers.push(entry);
@@ -40737,9 +40750,6 @@ function detectClient(relativePath) {
40737
40750
  }
40738
40751
  return "claude-code";
40739
40752
  }
40740
- function detectScope(_relativePath) {
40741
- return "project";
40742
- }
40743
40753
  function findRootKey(parsed) {
40744
40754
  if ("mcpServers" in parsed) return "mcpServers";
40745
40755
  if ("servers" in parsed) return "servers";
@@ -40756,15 +40766,24 @@ function inferTransport(raw) {
40756
40766
  if ("url" in raw) return "http";
40757
40767
  return "unknown";
40758
40768
  }
40759
- function findServerLine(lines, serverName) {
40760
- const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40769
+ function findRootKeyLine(lines, rootKey) {
40770
+ const escaped = rootKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40761
40771
  const pattern = new RegExp(`"${escaped}"\\s*:`);
40762
40772
  for (let i2 = 0; i2 < lines.length; i2++) {
40773
+ if (pattern.test(lines[i2])) return i2;
40774
+ }
40775
+ return -1;
40776
+ }
40777
+ function findServerLine(lines, serverName, rootKeyLine) {
40778
+ const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40779
+ const pattern = new RegExp(`"${escaped}"\\s*:`);
40780
+ const start = rootKeyLine >= 0 ? rootKeyLine + 1 : 0;
40781
+ for (let i2 = start; i2 < lines.length; i2++) {
40763
40782
  if (pattern.test(lines[i2])) {
40764
40783
  return i2 + 1;
40765
40784
  }
40766
40785
  }
40767
- return 1;
40786
+ return rootKeyLine >= 0 ? rootKeyLine + 1 : 1;
40768
40787
  }
40769
40788
  function isStringRecord(value) {
40770
40789
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
@@ -41296,11 +41315,11 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
41296
41315
  onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
41297
41316
  ensurePropertyComplete(offset + length);
41298
41317
  },
41299
- onSeparator: (sep, offset, length) => {
41318
+ onSeparator: (sep2, offset, length) => {
41300
41319
  if (currentParent.type === "property") {
41301
- if (sep === ":") {
41320
+ if (sep2 === ":") {
41302
41321
  currentParent.colonOffset = offset;
41303
- } else if (sep === ",") {
41322
+ } else if (sep2 === ",") {
41304
41323
  ensurePropertyComplete(offset);
41305
41324
  }
41306
41325
  }
@@ -41746,9 +41765,9 @@ var init_main = __esm({
41746
41765
  });
41747
41766
 
41748
41767
  // src/core/mcph-parser.ts
41749
- async function parseMchpConfig(file2, projectRoot, scopeOverride) {
41768
+ async function parseMcphConfig(file2, projectRoot, scopeOverride) {
41750
41769
  const content = readFileContent(file2.absolutePath);
41751
- const scope = scopeOverride ?? detectScope2(file2.relativePath);
41770
+ const scope = scopeOverride ?? detectScope(file2.relativePath);
41752
41771
  const isGitTracked = await checkGitTracked2(file2.absolutePath, projectRoot);
41753
41772
  const isGitignored = await checkGitignored(file2.absolutePath, projectRoot);
41754
41773
  const result = {
@@ -41806,9 +41825,8 @@ async function parseMchpConfig(file2, projectRoot, scopeOverride) {
41806
41825
  }
41807
41826
  return result;
41808
41827
  }
41809
- function detectScope2(relativePath) {
41828
+ function detectScope(relativePath) {
41810
41829
  const normalized = relativePath.replace(/\\/g, "/");
41811
- if (normalized.startsWith("~/")) return "global";
41812
41830
  if (normalized.endsWith(".mcph.local.json")) return "project-local";
41813
41831
  return "project";
41814
41832
  }
@@ -42168,29 +42186,33 @@ async function checkPaths(file2, projectRoot) {
42168
42186
  function findClosestMatch(target, files) {
42169
42187
  const targetNorm = target.replace(/\\/g, "/");
42170
42188
  const targetBase = path3.basename(targetNorm);
42171
- let bestMatch = null;
42172
- let bestDistance = Infinity;
42189
+ let basenameMatch = null;
42190
+ let basenameDistance = Infinity;
42173
42191
  for (const file2 of files) {
42174
42192
  const fileNorm = file2.replace(/\\/g, "/");
42175
42193
  if (path3.basename(fileNorm) === targetBase && fileNorm !== targetNorm) {
42176
42194
  const dist = levenshtein(targetNorm, fileNorm);
42177
- if (dist < bestDistance) {
42178
- bestDistance = dist;
42179
- bestMatch = fileNorm;
42195
+ if (dist < basenameDistance) {
42196
+ basenameDistance = dist;
42197
+ basenameMatch = fileNorm;
42180
42198
  }
42181
42199
  }
42182
42200
  }
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
- }
42201
+ if (basenameMatch) return basenameMatch;
42202
+ const absoluteCap = Math.max(targetNorm.length * 0.4, 5);
42203
+ let fullPathMatch = null;
42204
+ let fullPathDistance = Infinity;
42205
+ for (const file2 of files) {
42206
+ const fileNorm = file2.replace(/\\/g, "/");
42207
+ const lenDelta = Math.abs(targetNorm.length - fileNorm.length);
42208
+ if (lenDelta >= fullPathDistance || lenDelta > absoluteCap) continue;
42209
+ const dist = levenshtein(targetNorm, fileNorm);
42210
+ if (dist < fullPathDistance && dist <= absoluteCap) {
42211
+ fullPathDistance = dist;
42212
+ fullPathMatch = fileNorm;
42191
42213
  }
42192
42214
  }
42193
- return bestMatch;
42215
+ return fullPathMatch;
42194
42216
  }
42195
42217
  var import_fast_levenshtein, levenshtein, cachedProjectFiles;
42196
42218
  var init_paths = __esm({
@@ -42208,6 +42230,24 @@ var init_paths = __esm({
42208
42230
  // src/core/checks/commands.ts
42209
42231
  import * as fs4 from "node:fs";
42210
42232
  import * as path4 from "node:path";
42233
+ function extractNpxPackage(cmd) {
42234
+ if (!/^npx\b/.test(cmd)) return null;
42235
+ const tokens = cmd.split(/\s+/).slice(1);
42236
+ for (let i2 = 0; i2 < tokens.length; i2++) {
42237
+ const t2 = tokens[i2];
42238
+ if (t2 === "-p" || t2 === "--package") {
42239
+ const v2 = tokens[i2 + 1];
42240
+ if (v2 && !v2.startsWith("-")) return v2;
42241
+ continue;
42242
+ }
42243
+ if (t2.startsWith("-p=") || t2.startsWith("--package=")) {
42244
+ return t2.slice(t2.indexOf("=") + 1) || null;
42245
+ }
42246
+ if (t2.startsWith("-")) continue;
42247
+ return t2;
42248
+ }
42249
+ return null;
42250
+ }
42211
42251
  async function checkCommands(file2, projectRoot) {
42212
42252
  const issues = [];
42213
42253
  const pkgJson = loadPackageJson(projectRoot);
@@ -42246,10 +42286,9 @@ async function checkCommands(file2, projectRoot) {
42246
42286
  }
42247
42287
  continue;
42248
42288
  }
42249
- const npxMatch = cmd.match(NPX_PATTERN);
42250
- if (npxMatch && pkgJson) {
42251
- const pkgName = npxMatch[1];
42252
- if (pkgName.startsWith("-")) continue;
42289
+ if (/^npx\b/.test(cmd) && pkgJson) {
42290
+ const pkgName = extractNpxPackage(cmd);
42291
+ if (!pkgName) continue;
42253
42292
  const allDeps = {
42254
42293
  ...pkgJson.dependencies,
42255
42294
  ...pkgJson.devDependencies,
@@ -42324,7 +42363,7 @@ async function checkCommands(file2, projectRoot) {
42324
42363
  }
42325
42364
  function loadMakefile(projectRoot) {
42326
42365
  try {
42327
- return fs4.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8");
42366
+ return stripBom(fs4.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8"));
42328
42367
  } catch {
42329
42368
  return null;
42330
42369
  }
@@ -42333,14 +42372,13 @@ function hasMakeTarget(makefile, target) {
42333
42372
  const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
42334
42373
  return pattern.test(makefile);
42335
42374
  }
42336
- var NPM_SCRIPT_PATTERN, MAKE_PATTERN, NPX_PATTERN;
42375
+ var NPM_SCRIPT_PATTERN, MAKE_PATTERN;
42337
42376
  var init_commands = __esm({
42338
42377
  "src/core/checks/commands.ts"() {
42339
42378
  "use strict";
42340
42379
  init_fs();
42341
42380
  NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
42342
42381
  MAKE_PATTERN = /^make\s+(\S+)/;
42343
- NPX_PATTERN = /^npx\s+(\S+)/;
42344
42382
  }
42345
42383
  });
42346
42384
 
@@ -42405,26 +42443,21 @@ var init_staleness = __esm({
42405
42443
  });
42406
42444
 
42407
42445
  // src/core/checks/tokens.ts
42408
- function setTokenThresholds(overrides) {
42409
- const merged = { ...DEFAULT_THRESHOLDS, ...overrides };
42446
+ function resolveTokenThresholds(overrides) {
42447
+ if (!overrides) return DEFAULT_TOKEN_THRESHOLDS;
42448
+ const merged = { ...DEFAULT_TOKEN_THRESHOLDS, ...overrides };
42410
42449
  if (merged.info >= merged.warning || merged.warning >= merged.error) {
42411
42450
  console.error(
42412
42451
  `Warning: token thresholds should satisfy info < warning < error (got ${merged.info}, ${merged.warning}, ${merged.error}) \u2014 using defaults`
42413
42452
  );
42414
- return;
42453
+ return DEFAULT_TOKEN_THRESHOLDS;
42415
42454
  }
42416
- currentThresholds = merged;
42455
+ return merged;
42417
42456
  }
42418
- function resetTokenThresholds() {
42419
- currentThresholds = DEFAULT_THRESHOLDS;
42420
- }
42421
- function getTokenThresholds() {
42422
- return currentThresholds;
42423
- }
42424
- async function checkTokens(file2, _projectRoot) {
42457
+ async function checkTokens(file2, _projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42425
42458
  const issues = [];
42426
42459
  const tokens = file2.totalTokens;
42427
- if (tokens >= currentThresholds.error) {
42460
+ if (tokens >= thresholds.error) {
42428
42461
  issues.push({
42429
42462
  severity: "error",
42430
42463
  check: "tokens",
@@ -42433,7 +42466,7 @@ async function checkTokens(file2, _projectRoot) {
42433
42466
  message: `${tokens.toLocaleString()} tokens \u2014 consumes significant context window space`,
42434
42467
  suggestion: "Consider splitting into focused sections or removing redundant content."
42435
42468
  });
42436
- } else if (tokens >= currentThresholds.warning) {
42469
+ } else if (tokens >= thresholds.warning) {
42437
42470
  issues.push({
42438
42471
  severity: "warning",
42439
42472
  check: "tokens",
@@ -42442,7 +42475,7 @@ async function checkTokens(file2, _projectRoot) {
42442
42475
  message: `${tokens.toLocaleString()} tokens \u2014 large context file`,
42443
42476
  suggestion: "Consider trimming \u2014 research shows diminishing returns past ~300 lines."
42444
42477
  });
42445
- } else if (tokens >= currentThresholds.info) {
42478
+ } else if (tokens >= thresholds.info) {
42446
42479
  issues.push({
42447
42480
  severity: "info",
42448
42481
  check: "tokens",
@@ -42453,9 +42486,9 @@ async function checkTokens(file2, _projectRoot) {
42453
42486
  }
42454
42487
  return issues;
42455
42488
  }
42456
- function checkAggregateTokens(files) {
42489
+ function checkAggregateTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42457
42490
  const total = files.reduce((sum, f) => sum + f.tokens, 0);
42458
- if (total > currentThresholds.aggregate && files.length > 1) {
42491
+ if (total > thresholds.aggregate && files.length > 1) {
42459
42492
  return {
42460
42493
  severity: "warning",
42461
42494
  check: "tokens",
@@ -42467,11 +42500,11 @@ function checkAggregateTokens(files) {
42467
42500
  }
42468
42501
  return null;
42469
42502
  }
42470
- var DEFAULT_THRESHOLDS, currentThresholds;
42503
+ var DEFAULT_TOKEN_THRESHOLDS;
42471
42504
  var init_tokens2 = __esm({
42472
42505
  "src/core/checks/tokens.ts"() {
42473
42506
  "use strict";
42474
- DEFAULT_THRESHOLDS = {
42507
+ DEFAULT_TOKEN_THRESHOLDS = {
42475
42508
  info: 1e3,
42476
42509
  warning: 3e3,
42477
42510
  error: 8e3,
@@ -42479,7 +42512,6 @@ var init_tokens2 = __esm({
42479
42512
  tierBreakdown: 1e3,
42480
42513
  tierAggregate: 4e3
42481
42514
  };
42482
- currentThresholds = DEFAULT_THRESHOLDS;
42483
42515
  }
42484
42516
  });
42485
42517
 
@@ -42515,7 +42547,14 @@ function isAlwaysLoaded(file2) {
42515
42547
  return !hasPathsFrontmatter(file2.content);
42516
42548
  }
42517
42549
  const basename4 = rel.split("/").pop() ?? "";
42518
- return ALWAYS_LOADED_BASENAMES.has(basename4);
42550
+ for (const name of ALWAYS_LOADED_NAMES) {
42551
+ if (name.includes("/")) {
42552
+ if (rel === name || rel.endsWith("/" + name)) return true;
42553
+ } else if (basename4 === name) {
42554
+ return true;
42555
+ }
42556
+ }
42557
+ return false;
42519
42558
  }
42520
42559
  function computeSectionCosts(file2) {
42521
42560
  if (file2.sections.length === 0) return [];
@@ -42537,7 +42576,7 @@ function loadSettingsSources(projectRoot) {
42537
42576
  for (const p2 of candidates) {
42538
42577
  let content;
42539
42578
  try {
42540
- content = fs5.readFileSync(p2, "utf-8");
42579
+ content = stripBom(fs5.readFileSync(p2, "utf-8"));
42541
42580
  } catch {
42542
42581
  continue;
42543
42582
  }
@@ -42553,9 +42592,17 @@ function canonicalizeCommand(backticked) {
42553
42592
  const beforeFlags = backticked.trim().split(/\s+--?/, 1)[0];
42554
42593
  return beforeFlags.replace(/\s+/g, " ");
42555
42594
  }
42595
+ function buildCommandPattern(cmd) {
42596
+ const tokens = cmd.split(/\s+/).filter(Boolean);
42597
+ const escaped = tokens.map((t2) => t2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
42598
+ if (tokens.length === 1) {
42599
+ return new RegExp(`(?<![A-Za-z0-9_\\-])${escaped[0]}(?![A-Za-z0-9_\\-])`, "i");
42600
+ }
42601
+ const body = escaped.join("[\\s\\-_]+");
42602
+ return new RegExp(`(?<![A-Za-z0-9])${body}(?![A-Za-z0-9])`, "i");
42603
+ }
42556
42604
  function commandIsEnforced(cmd, settings) {
42557
- const escaped = cmd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42558
- const pattern = new RegExp(`\\b${escaped}\\b`, "i");
42605
+ const pattern = buildCommandPattern(cmd);
42559
42606
  for (const s of settings) {
42560
42607
  for (const entry of s.permissions?.deny ?? []) {
42561
42608
  if (pattern.test(entry)) return true;
@@ -42590,10 +42637,10 @@ function checkHardEnforcement(file2, settings) {
42590
42637
  }
42591
42638
  return issues;
42592
42639
  }
42593
- async function checkTierTokens(file2, projectRoot) {
42640
+ async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42594
42641
  if (!isAlwaysLoaded(file2)) return [];
42595
42642
  const issues = [];
42596
- const threshold = getTokenThresholds().tierBreakdown;
42643
+ const threshold = thresholds.tierBreakdown;
42597
42644
  if (file2.totalTokens >= threshold) {
42598
42645
  const sectionCosts = computeSectionCosts(file2);
42599
42646
  if (sectionCosts.length > 0) {
@@ -42616,11 +42663,11 @@ async function checkTierTokens(file2, projectRoot) {
42616
42663
  issues.push(...checkHardEnforcement(file2, settings));
42617
42664
  return issues;
42618
42665
  }
42619
- function checkAggregateTierTokens(files) {
42666
+ function checkAggregateTierTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42620
42667
  const alwaysLoaded = files.filter(isAlwaysLoaded);
42621
42668
  if (alwaysLoaded.length < 2) return null;
42622
42669
  const total = alwaysLoaded.reduce((sum, f) => sum + f.totalTokens, 0);
42623
- const threshold = getTokenThresholds().tierAggregate;
42670
+ const threshold = thresholds.tierAggregate;
42624
42671
  if (total < threshold) return null;
42625
42672
  const breakdown = alwaysLoaded.slice().sort((a, b2) => b2.totalTokens - a.totalTokens).slice(0, 5).map((f) => ` - ${f.relativePath}: ~${f.totalTokens.toLocaleString()} tokens`).join("\n");
42626
42673
  return {
@@ -42633,13 +42680,14 @@ function checkAggregateTierTokens(files) {
42633
42680
  suggestion: "Consider moving the largest files or their heaviest sections to on-demand tiers (skills, subagents, memory)."
42634
42681
  };
42635
42682
  }
42636
- var ALWAYS_LOADED_BASENAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
42683
+ var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
42637
42684
  var init_tier_tokens = __esm({
42638
42685
  "src/core/checks/tier-tokens.ts"() {
42639
42686
  "use strict";
42640
42687
  init_tokens();
42688
+ init_fs();
42641
42689
  init_tokens2();
42642
- ALWAYS_LOADED_BASENAMES = /* @__PURE__ */ new Set([
42690
+ ALWAYS_LOADED_NAMES = [
42643
42691
  "CLAUDE.md",
42644
42692
  "CLAUDE.local.md",
42645
42693
  "AGENTS.md",
@@ -42654,15 +42702,42 @@ var init_tier_tokens = __esm({
42654
42702
  ".rules",
42655
42703
  ".goosehints",
42656
42704
  "replit.md",
42657
- "copilot-instructions.md",
42658
- "guidelines.md",
42659
- "instructions.md"
42660
- ]);
42705
+ ".github/copilot-instructions.md",
42706
+ ".junie/guidelines.md",
42707
+ ".junie/AGENTS.md",
42708
+ ".goose/instructions.md"
42709
+ ];
42661
42710
  TOP_SECTIONS_TO_REPORT = 3;
42662
42711
  INVIOLABLE_WITH_COMMAND = /\b(NEVER|ALWAYS|DON'?T|DO NOT|MUST NOT)\b[^.!?`]{0,80}`([^`]+)`/i;
42663
42712
  }
42664
42713
  });
42665
42714
 
42715
+ // src/utils/similarity.ts
42716
+ function jaccardSimilarityFromSets(a, b2, opts = {}) {
42717
+ const bothEmptyIsIdentical = opts.bothEmptyIsIdentical ?? false;
42718
+ if (a.size === 0 && b2.size === 0) {
42719
+ return bothEmptyIsIdentical ? 1 : 0;
42720
+ }
42721
+ if (a.size === 0 || b2.size === 0) return 0;
42722
+ const [small, large] = a.size <= b2.size ? [a, b2] : [b2, a];
42723
+ let intersection2 = 0;
42724
+ for (const line of small) {
42725
+ if (large.has(line)) intersection2++;
42726
+ }
42727
+ const unionSize = a.size + b2.size - intersection2;
42728
+ return intersection2 / unionSize;
42729
+ }
42730
+ function toLineSet(text, minTokenLen) {
42731
+ return new Set(
42732
+ text.split("\n").map((l) => l.trim()).filter((l) => l.length > minTokenLen)
42733
+ );
42734
+ }
42735
+ var init_similarity = __esm({
42736
+ "src/utils/similarity.ts"() {
42737
+ "use strict";
42738
+ }
42739
+ });
42740
+
42666
42741
  // src/core/checks/redundancy.ts
42667
42742
  import * as path7 from "node:path";
42668
42743
  function compilePatterns(allDeps) {
@@ -42685,6 +42760,20 @@ function compilePatterns(allDeps) {
42685
42760
  }
42686
42761
  return compiled;
42687
42762
  }
42763
+ function getCompiledPatterns(projectRoot, allDeps) {
42764
+ const relevant = [];
42765
+ for (const pkg of Object.keys(PACKAGE_TECH_MAP)) {
42766
+ if (allDeps.has(pkg)) relevant.push(pkg);
42767
+ }
42768
+ relevant.sort();
42769
+ const key = `${projectRoot}\0${relevant.join(" ")}`;
42770
+ let compiled = compiledPatternsCache.get(key);
42771
+ if (!compiled) {
42772
+ compiled = compilePatterns(allDeps);
42773
+ compiledPatternsCache.set(key, compiled);
42774
+ }
42775
+ return compiled;
42776
+ }
42688
42777
  async function checkRedundancy(file2, projectRoot) {
42689
42778
  const issues = [];
42690
42779
  const pkgJson = loadPackageJson(projectRoot);
@@ -42695,7 +42784,7 @@ async function checkRedundancy(file2, projectRoot) {
42695
42784
  ...Object.keys(pkgJson.peerDependencies || {}),
42696
42785
  ...Object.keys(pkgJson.optionalDependencies || {})
42697
42786
  ]);
42698
- const compiledPatterns = compilePatterns(allDeps);
42787
+ const compiledPatterns = getCompiledPatterns(projectRoot, allDeps);
42699
42788
  const lines2 = file2.content.split("\n");
42700
42789
  for (let i2 = 0; i2 < lines2.length; i2++) {
42701
42790
  const line = lines2[i2];
@@ -42746,10 +42835,14 @@ async function checkRedundancy(file2, projectRoot) {
42746
42835
  }
42747
42836
  function checkDuplicateContent(files) {
42748
42837
  const issues = [];
42749
- const DUPLICATE_CONTENT_THRESHOLD = 0.6;
42838
+ const lineSets = files.map((f) => toLineSet(f.content, DUPLICATE_CONTENT_MIN_TOKEN_LEN));
42750
42839
  for (let i2 = 0; i2 < files.length; i2++) {
42840
+ const a = lineSets[i2];
42841
+ if (a.size === 0) continue;
42751
42842
  for (let j3 = i2 + 1; j3 < files.length; j3++) {
42752
- const overlap = calculateLineOverlap(files[i2].content, files[j3].content);
42843
+ const b2 = lineSets[j3];
42844
+ if (b2.size === 0) continue;
42845
+ const overlap = jaccardSimilarityFromSets(a, b2);
42753
42846
  if (overlap >= DUPLICATE_CONTENT_THRESHOLD) {
42754
42847
  issues.push({
42755
42848
  severity: "warning",
@@ -42764,29 +42857,15 @@ function checkDuplicateContent(files) {
42764
42857
  }
42765
42858
  return issues;
42766
42859
  }
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
42860
  function escapeRegex2(str) {
42783
42861
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42784
42862
  }
42785
- var PACKAGE_TECH_MAP;
42863
+ var PACKAGE_TECH_MAP, compiledPatternsCache, DUPLICATE_CONTENT_THRESHOLD, DUPLICATE_CONTENT_MIN_TOKEN_LEN;
42786
42864
  var init_redundancy = __esm({
42787
42865
  "src/core/checks/redundancy.ts"() {
42788
42866
  "use strict";
42789
42867
  init_fs();
42868
+ init_similarity();
42790
42869
  init_tokens();
42791
42870
  PACKAGE_TECH_MAP = {
42792
42871
  react: ["React", "react"],
@@ -42839,6 +42918,9 @@ var init_redundancy = __esm({
42839
42918
  cypress: ["Cypress"],
42840
42919
  puppeteer: ["Puppeteer"]
42841
42920
  };
42921
+ compiledPatternsCache = /* @__PURE__ */ new Map();
42922
+ DUPLICATE_CONTENT_THRESHOLD = 0.6;
42923
+ DUPLICATE_CONTENT_MIN_TOKEN_LEN = 10;
42842
42924
  }
42843
42925
  });
42844
42926
 
@@ -43156,7 +43238,7 @@ var init_contradictions = __esm({
43156
43238
  function parseFrontmatter(content) {
43157
43239
  const lines = content.split("\n");
43158
43240
  if (lines[0]?.trim() !== "---") {
43159
- return { found: false, fields: {}, endLine: 0 };
43241
+ return { found: false, fields: {}, endLine: 0, unclosed: false };
43160
43242
  }
43161
43243
  const fields = {};
43162
43244
  let endLine = 0;
@@ -43183,9 +43265,9 @@ function parseFrontmatter(content) {
43183
43265
  }
43184
43266
  }
43185
43267
  if (endLine === 0) {
43186
- return { found: true, fields, endLine: lines.length };
43268
+ return { found: true, fields, endLine: lines.length, unclosed: true };
43187
43269
  }
43188
- return { found: true, fields, endLine };
43270
+ return { found: true, fields, endLine, unclosed: false };
43189
43271
  }
43190
43272
  function isCursorMdc(file2) {
43191
43273
  return file2.relativePath.endsWith(".mdc");
@@ -43196,6 +43278,23 @@ function isCopilotInstructions(file2) {
43196
43278
  function isWindsurfRule(file2) {
43197
43279
  return file2.relativePath.includes(".windsurf/rules/") && file2.relativePath.endsWith(".md");
43198
43280
  }
43281
+ function hasUnbalancedBracketsOrQuotes(val) {
43282
+ let square = 0;
43283
+ let curly = 0;
43284
+ for (const ch of val) {
43285
+ if (ch === "[") square++;
43286
+ else if (ch === "]") square--;
43287
+ else if (ch === "{") curly++;
43288
+ else if (ch === "}") curly--;
43289
+ if (square < 0 || curly < 0) return true;
43290
+ }
43291
+ if (square !== 0 || curly !== 0) return true;
43292
+ const doubleQuotes = (val.match(/"/g) || []).length;
43293
+ const singleQuotes = (val.match(/'/g) || []).length;
43294
+ if (doubleQuotes % 2 !== 0) return true;
43295
+ if (singleQuotes % 2 !== 0) return true;
43296
+ return false;
43297
+ }
43199
43298
  async function checkFrontmatter(file2, _projectRoot) {
43200
43299
  const issues = [];
43201
43300
  if (isCursorMdc(file2)) {
@@ -43207,9 +43306,23 @@ async function checkFrontmatter(file2, _projectRoot) {
43207
43306
  }
43208
43307
  return issues;
43209
43308
  }
43309
+ function unclosedFrontmatterIssue() {
43310
+ return {
43311
+ severity: "error",
43312
+ check: "frontmatter",
43313
+ ruleId: "frontmatter/unclosed",
43314
+ line: 1,
43315
+ message: "Frontmatter opens with `---` but is never closed",
43316
+ suggestion: "Add a matching `---` line (with no leading whitespace) after the last frontmatter field"
43317
+ };
43318
+ }
43210
43319
  function validateCursorMdc(file2) {
43211
43320
  const issues = [];
43212
43321
  const fm = parseFrontmatter(file2.content);
43322
+ if (fm.unclosed) {
43323
+ issues.push(unclosedFrontmatterIssue());
43324
+ return issues;
43325
+ }
43213
43326
  if (!fm.found) {
43214
43327
  issues.push({
43215
43328
  severity: "warning",
@@ -43256,13 +43369,13 @@ function validateCursorMdc(file2) {
43256
43369
  }
43257
43370
  if ("globs" in fm.fields) {
43258
43371
  const val = fm.fields["globs"];
43259
- if (val && !val.startsWith("[") && !val.startsWith('"') && !val.includes("*") && !val.includes("/")) {
43372
+ if (val && hasUnbalancedBracketsOrQuotes(val)) {
43260
43373
  issues.push({
43261
43374
  severity: "warning",
43262
43375
  check: "frontmatter",
43263
43376
  ruleId: "frontmatter/invalid-value",
43264
43377
  line: 1,
43265
- message: `Possibly invalid globs value: "${val}"`,
43378
+ message: `Possibly malformed globs value: "${val}"`,
43266
43379
  suggestion: 'globs should be a glob pattern like "src/**/*.ts" or an array like ["*.ts", "*.tsx"]'
43267
43380
  });
43268
43381
  }
@@ -43272,6 +43385,10 @@ function validateCursorMdc(file2) {
43272
43385
  function validateCopilotInstructions(file2) {
43273
43386
  const issues = [];
43274
43387
  const fm = parseFrontmatter(file2.content);
43388
+ if (fm.unclosed) {
43389
+ issues.push(unclosedFrontmatterIssue());
43390
+ return issues;
43391
+ }
43275
43392
  if (!fm.found) {
43276
43393
  issues.push({
43277
43394
  severity: "info",
@@ -43298,6 +43415,10 @@ function validateCopilotInstructions(file2) {
43298
43415
  function validateWindsurfRule(file2) {
43299
43416
  const issues = [];
43300
43417
  const fm = parseFrontmatter(file2.content);
43418
+ if (fm.unclosed) {
43419
+ issues.push(unclosedFrontmatterIssue());
43420
+ return issues;
43421
+ }
43301
43422
  if (!fm.found) {
43302
43423
  issues.push({
43303
43424
  severity: "info",
@@ -43349,7 +43470,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43349
43470
  issues.push({
43350
43471
  severity: "error",
43351
43472
  check: "mcp-schema",
43352
- ruleId: "invalid-json",
43473
+ ruleId: "mcp-schema/invalid-json",
43353
43474
  line: 1,
43354
43475
  message: `MCP config is not valid JSON: ${err}`
43355
43476
  });
@@ -43360,7 +43481,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43360
43481
  issues.push({
43361
43482
  severity: "error",
43362
43483
  check: "mcp-schema",
43363
- ruleId: "missing-root-key",
43484
+ ruleId: "mcp-schema/missing-root-key",
43364
43485
  line: 1,
43365
43486
  message: `MCP config has no "${config2.expectedRootKey}" key`
43366
43487
  });
@@ -43371,7 +43492,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43371
43492
  issues.push({
43372
43493
  severity: "error",
43373
43494
  check: "mcp-schema",
43374
- ruleId: "wrong-root-key",
43495
+ ruleId: "mcp-schema/wrong-root-key",
43375
43496
  line,
43376
43497
  message: `${config2.relativePath} must use "${config2.expectedRootKey}" as root key, not "${config2.actualRootKey}"`,
43377
43498
  fix: {
@@ -43386,7 +43507,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43386
43507
  issues.push({
43387
43508
  severity: "info",
43388
43509
  check: "mcp-schema",
43389
- ruleId: "empty-servers",
43510
+ ruleId: "mcp-schema/empty-servers",
43390
43511
  line: 1,
43391
43512
  message: "MCP config has no server entries"
43392
43513
  });
@@ -43397,7 +43518,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43397
43518
  issues.push({
43398
43519
  severity: "error",
43399
43520
  check: "mcp-schema",
43400
- ruleId: "no-name-field",
43521
+ ruleId: "mcp-schema/no-name-field",
43401
43522
  line: server2.line,
43402
43523
  message: "Server name cannot be empty"
43403
43524
  });
@@ -43409,7 +43530,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43409
43530
  issues.push({
43410
43531
  severity: "warning",
43411
43532
  check: "mcp-schema",
43412
- ruleId: "unknown-transport",
43533
+ ruleId: "mcp-schema/unknown-transport",
43413
43534
  line: server2.line,
43414
43535
  message: `Server "${server2.name}" has unknown transport type "${typeVal}"`
43415
43536
  });
@@ -43419,7 +43540,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43419
43540
  issues.push({
43420
43541
  severity: "warning",
43421
43542
  check: "mcp-schema",
43422
- ruleId: "ambiguous-transport",
43543
+ ruleId: "mcp-schema/ambiguous-transport",
43423
43544
  line: server2.line,
43424
43545
  message: `Server "${server2.name}" has both "command" and "url" \u2014 transport is ambiguous`
43425
43546
  });
@@ -43428,7 +43549,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43428
43549
  issues.push({
43429
43550
  severity: "error",
43430
43551
  check: "mcp-schema",
43431
- ruleId: "missing-command",
43552
+ ruleId: "mcp-schema/missing-command",
43432
43553
  line: server2.line,
43433
43554
  message: `Server "${server2.name}" has no "command" field`
43434
43555
  });
@@ -43437,7 +43558,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43437
43558
  issues.push({
43438
43559
  severity: "error",
43439
43560
  check: "mcp-schema",
43440
- ruleId: "missing-url",
43561
+ ruleId: "mcp-schema/missing-url",
43441
43562
  line: server2.line,
43442
43563
  message: `Server "${server2.name}" has no "url" field`
43443
43564
  });
@@ -43469,6 +43590,10 @@ function isEnvVarRef(value) {
43469
43590
  function isKnownApiKey(value) {
43470
43591
  return API_KEY_PATTERNS.some((p2) => p2.test(value));
43471
43592
  }
43593
+ function nameSuggestsSecret(name) {
43594
+ const upper = name.toUpperCase();
43595
+ return SECRET_NAME_KEYWORDS.some((kw) => upper.includes(kw));
43596
+ }
43472
43597
  function isHighEntropySecret(value) {
43473
43598
  if (isEnvVarRef(value)) return false;
43474
43599
  return HIGH_ENTROPY_PATTERN.test(value);
@@ -43492,7 +43617,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43492
43617
  issues.push({
43493
43618
  severity: "error",
43494
43619
  check: "mcp-security",
43495
- ruleId: "hardcoded-bearer",
43620
+ ruleId: "mcp-security/hardcoded-bearer",
43496
43621
  line: server2.line,
43497
43622
  message: `Server "${server2.name}" has a hardcoded Bearer token in a git-tracked file`,
43498
43623
  fix: {
@@ -43509,7 +43634,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43509
43634
  issues.push({
43510
43635
  severity: "error",
43511
43636
  check: "mcp-security",
43512
- ruleId: "hardcoded-api-key",
43637
+ ruleId: "mcp-security/hardcoded-api-key",
43513
43638
  line: server2.line,
43514
43639
  message: `Server "${server2.name}" has a hardcoded API key in a git-tracked file`
43515
43640
  });
@@ -43517,13 +43642,14 @@ async function checkMcpSecurity(config2, _projectRoot) {
43517
43642
  }
43518
43643
  }
43519
43644
  if (server2.env) {
43520
- for (const envValue of Object.values(server2.env)) {
43521
- if (!isEnvVarRef(envValue) && (isKnownApiKey(envValue) || isHighEntropySecret(envValue))) {
43645
+ for (const [envName, envValue] of Object.entries(server2.env)) {
43646
+ const isSecret = isKnownApiKey(envValue) || nameSuggestsSecret(envName) && isHighEntropySecret(envValue);
43647
+ if (!isEnvVarRef(envValue) && isSecret) {
43522
43648
  const envVar = deriveEnvVarName(server2.name, "API_KEY");
43523
43649
  issues.push({
43524
43650
  severity: "error",
43525
43651
  check: "mcp-security",
43526
- ruleId: "hardcoded-api-key",
43652
+ ruleId: "mcp-security/hardcoded-api-key",
43527
43653
  line: server2.line,
43528
43654
  message: `Server "${server2.name}" has a hardcoded API key in a git-tracked file`,
43529
43655
  fix: {
@@ -43540,7 +43666,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43540
43666
  issues.push({
43541
43667
  severity: "error",
43542
43668
  check: "mcp-security",
43543
- ruleId: "secret-in-url",
43669
+ ruleId: "mcp-security/secret-in-url",
43544
43670
  line: server2.line,
43545
43671
  message: `Server "${server2.name}" has a secret in the URL query string`
43546
43672
  });
@@ -43553,7 +43679,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43553
43679
  issues.push({
43554
43680
  severity: "warning",
43555
43681
  check: "mcp-security",
43556
- ruleId: "http-no-tls",
43682
+ ruleId: "mcp-security/http-no-tls",
43557
43683
  line: server2.line,
43558
43684
  message: `Server "${server2.name}" uses HTTP without TLS`
43559
43685
  });
@@ -43565,7 +43691,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43565
43691
  }
43566
43692
  return issues;
43567
43693
  }
43568
- var API_KEY_PATTERNS, ENV_VAR_REF, HIGH_ENTROPY_PATTERN, URL_SECRET_PARAMS;
43694
+ var API_KEY_PATTERNS, ENV_VAR_REF, HIGH_ENTROPY_PATTERN, URL_SECRET_PARAMS, SECRET_NAME_KEYWORDS;
43569
43695
  var init_security = __esm({
43570
43696
  "src/core/checks/mcp/security.ts"() {
43571
43697
  "use strict";
@@ -43594,6 +43720,22 @@ var init_security = __esm({
43594
43720
  ENV_VAR_REF = /\$\{[^}]+\}/;
43595
43721
  HIGH_ENTROPY_PATTERN = /^[A-Za-z0-9+/=_-]{21,}$/;
43596
43722
  URL_SECRET_PARAMS = /[?&](key|token|api_key|apikey|secret|password|access_token)=/i;
43723
+ SECRET_NAME_KEYWORDS = [
43724
+ "KEY",
43725
+ "TOKEN",
43726
+ "SECRET",
43727
+ "PASSWORD",
43728
+ "PASSWD",
43729
+ "PASS",
43730
+ "AUTH",
43731
+ "CREDENTIAL",
43732
+ "CREDENTIALS",
43733
+ "APIKEY",
43734
+ "PRIVATE",
43735
+ "SIGNING",
43736
+ "SESSION",
43737
+ "COOKIE"
43738
+ ];
43597
43739
  }
43598
43740
  });
43599
43741
 
@@ -43609,7 +43751,7 @@ async function checkMcpCommands(config2, projectRoot) {
43609
43751
  issues.push({
43610
43752
  severity: "error",
43611
43753
  check: "mcp-commands",
43612
- ruleId: "windows-npx-no-wrapper",
43754
+ ruleId: "mcp-commands/windows-npx-no-wrapper",
43613
43755
  line: server2.line,
43614
43756
  message: `Server "${server2.name}": npx requires "cmd /c" wrapper on Windows`,
43615
43757
  suggestion: 'Change "command" to "cmd" and prepend "/c", "npx" to args \u2014 e.g. "args": ["/c", "npx", ...]'
@@ -43621,7 +43763,7 @@ async function checkMcpCommands(config2, projectRoot) {
43621
43763
  issues.push({
43622
43764
  severity: "warning",
43623
43765
  check: "mcp-commands",
43624
- ruleId: "command-not-found",
43766
+ ruleId: "mcp-commands/command-not-found",
43625
43767
  line: server2.line,
43626
43768
  message: `Server "${server2.name}": command "${server2.command}" not found`
43627
43769
  });
@@ -43636,7 +43778,7 @@ async function checkMcpCommands(config2, projectRoot) {
43636
43778
  issues.push({
43637
43779
  severity: "warning",
43638
43780
  check: "mcp-commands",
43639
- ruleId: "args-path-missing",
43781
+ ruleId: "mcp-commands/args-path-missing",
43640
43782
  line: server2.line,
43641
43783
  message: `Server "${server2.name}": arg "${arg}" looks like a file path but doesn't exist`
43642
43784
  });
@@ -43675,7 +43817,7 @@ async function checkMcpDeprecated(config2, _projectRoot) {
43675
43817
  issues.push({
43676
43818
  severity: "warning",
43677
43819
  check: "mcp-deprecated",
43678
- ruleId: "sse-transport",
43820
+ ruleId: "mcp-deprecated/sse-transport",
43679
43821
  line,
43680
43822
  message: `Server "${server2.name}" uses deprecated SSE transport \u2014 use "http" (Streamable HTTP) instead`,
43681
43823
  fix: {
@@ -43771,7 +43913,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43771
43913
  issues.push({
43772
43914
  severity: "error",
43773
43915
  check: "mcp-env",
43774
- ruleId: "wrong-syntax",
43916
+ ruleId: "mcp-env/wrong-syntax",
43775
43917
  line: server2.line,
43776
43918
  message: `Server "${server2.name}": Claude Code uses \${VAR}, not \${env:VAR}`,
43777
43919
  fix: buildSyntaxFix(config2, server2.line, value, "claude-code")
@@ -43783,7 +43925,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43783
43925
  issues.push({
43784
43926
  severity: "error",
43785
43927
  check: "mcp-env",
43786
- ruleId: "wrong-syntax",
43928
+ ruleId: "mcp-env/wrong-syntax",
43787
43929
  line: server2.line,
43788
43930
  message: `Server "${server2.name}": Cursor uses \${env:VAR}, not \${VAR}`,
43789
43931
  fix: buildSyntaxFix(config2, server2.line, value, "cursor")
@@ -43795,7 +43937,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43795
43937
  issues.push({
43796
43938
  severity: "error",
43797
43939
  check: "mcp-env",
43798
- ruleId: "wrong-syntax",
43940
+ ruleId: "mcp-env/wrong-syntax",
43799
43941
  line: server2.line,
43800
43942
  message: `Server "${server2.name}": Continue uses \${{ secrets.VAR }}, not \${VAR}`,
43801
43943
  fix: buildSyntaxFix(config2, server2.line, value, "continue")
@@ -43811,7 +43953,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43811
43953
  issues.push({
43812
43954
  severity: "info",
43813
43955
  check: "mcp-env",
43814
- ruleId: "unset-variable",
43956
+ ruleId: "mcp-env/unset-variable",
43815
43957
  line: server2.line,
43816
43958
  message: `Server "${server2.name}": environment variable "${ref.varName}" is not set`
43817
43959
  });
@@ -43823,7 +43965,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43823
43965
  issues.push({
43824
43966
  severity: "info",
43825
43967
  check: "mcp-env",
43826
- ruleId: "empty-env-block",
43968
+ ruleId: "mcp-env/empty-env-block",
43827
43969
  line: server2.line,
43828
43970
  message: `Server "${server2.name}": empty "env" block can be removed`
43829
43971
  });
@@ -43879,7 +44021,7 @@ async function checkMcpUrls(config2, _projectRoot) {
43879
44021
  issues.push({
43880
44022
  severity: "error",
43881
44023
  check: "mcp-urls",
43882
- ruleId: "malformed-url",
44024
+ ruleId: "mcp-urls/malformed-url",
43883
44025
  line: server2.line,
43884
44026
  message: `Server "${server2.name}": invalid URL "${server2.url}"`
43885
44027
  });
@@ -43889,7 +44031,7 @@ async function checkMcpUrls(config2, _projectRoot) {
43889
44031
  issues.push({
43890
44032
  severity: "warning",
43891
44033
  check: "mcp-urls",
43892
- ruleId: "localhost-in-project-config",
44034
+ ruleId: "mcp-urls/localhost-in-project-config",
43893
44035
  line: server2.line,
43894
44036
  message: `Server "${server2.name}": localhost URL in project config won't work for teammates`
43895
44037
  });
@@ -43898,7 +44040,7 @@ async function checkMcpUrls(config2, _projectRoot) {
43898
44040
  issues.push({
43899
44041
  severity: "info",
43900
44042
  check: "mcp-urls",
43901
- ruleId: "missing-path",
44043
+ ruleId: "mcp-urls/missing-path",
43902
44044
  line: server2.line,
43903
44045
  message: `Server "${server2.name}": URL has no path \u2014 most MCP servers expect /mcp`
43904
44046
  });
@@ -43946,7 +44088,7 @@ async function checkMcpConsistency(configs) {
43946
44088
  issues.push({
43947
44089
  severity: "warning",
43948
44090
  check: "mcp-consistency",
43949
- ruleId: "same-server-different-config",
44091
+ ruleId: "mcp-consistency/same-server-different-config",
43950
44092
  line: a.line,
43951
44093
  message: `Server "${name}" is configured differently in ${a.config.relativePath} and ${b2.config.relativePath}`
43952
44094
  });
@@ -43972,7 +44114,7 @@ function checkMissingFromClient(configs) {
43972
44114
  issues.push({
43973
44115
  severity: "info",
43974
44116
  check: "mcp-consistency",
43975
- ruleId: "missing-from-client",
44117
+ ruleId: "mcp-consistency/missing-from-client",
43976
44118
  line: primaryServer.line,
43977
44119
  message: `Server "${primaryServer.name}" is in .mcp.json but missing from ${other.relativePath}`
43978
44120
  });
@@ -44049,7 +44191,7 @@ function checkSingleFileIssues(configs) {
44049
44191
  issues.push({
44050
44192
  severity: "warning",
44051
44193
  check: "mcp-consistency",
44052
- ruleId: "duplicate-server-name",
44194
+ ruleId: "mcp-consistency/duplicate-server-name",
44053
44195
  line: 1,
44054
44196
  message: `Duplicate server name "${name}" in ${config2.relativePath} \u2014 only the last definition is used`
44055
44197
  });
@@ -44073,7 +44215,7 @@ async function checkMcpRedundancy(configs) {
44073
44215
  issues.push({
44074
44216
  severity: "info",
44075
44217
  check: "mcp-redundancy",
44076
- ruleId: "disabled-server",
44218
+ ruleId: "mcp-redundancy/disabled-server",
44077
44219
  line: server2.line,
44078
44220
  message: `Server "${server2.name}" is disabled \u2014 consider removing it if no longer needed`
44079
44221
  });
@@ -44103,7 +44245,7 @@ async function checkMcpRedundancy(configs) {
44103
44245
  issues.push({
44104
44246
  severity: "info",
44105
44247
  check: "mcp-redundancy",
44106
- ruleId: "identical-across-scopes",
44248
+ ruleId: "mcp-redundancy/identical-across-scopes",
44107
44249
  line: projectServer.line,
44108
44250
  message: `Server "${projectServer.name}" is identically configured in both ${projectConfig.relativePath} and ${globalConfig2.relativePath}`
44109
44251
  });
@@ -44129,7 +44271,7 @@ async function checkMcphTokenSecurity(config2, _projectRoot, options = {}) {
44129
44271
  issues.push({
44130
44272
  severity: "error",
44131
44273
  check: "mcph-token-security",
44132
- ruleId: "mcph-config/invalid-token-format",
44274
+ ruleId: "mcph-token-security/invalid-token-format",
44133
44275
  line: tokenPos.line,
44134
44276
  message: `"token" does not match expected format ^mcp_pat_[A-Za-z0-9_-]+$`,
44135
44277
  suggestion: `Check that the token was copied in full. A valid mcp.hosting PAT looks like: mcp_pat_aBcDeFg123...
@@ -44140,7 +44282,7 @@ If the token was truncated or wrapped in quotes elsewhere, re-issue it from http
44140
44282
  issues.push({
44141
44283
  severity: "error",
44142
44284
  check: "mcph-token-security",
44143
- ruleId: "mcph-config/token-in-project-scope",
44285
+ ruleId: "mcph-token-security/token-in-project-scope",
44144
44286
  line: tokenPos.line,
44145
44287
  message: `"token" in a git-tracked project-scope .mcph.json \u2014 PAT will leak via git history`,
44146
44288
  suggestion: `Delete line ${tokenPos.line} (the "token" field) from ${config2.relativePath}.
@@ -44158,7 +44300,7 @@ If this token was already committed, ROTATE it now: https://mcp.hosting/settings
44158
44300
  issues.push({
44159
44301
  severity,
44160
44302
  check: "mcph-token-security",
44161
- ruleId: "mcph-config/prefer-env-token",
44303
+ ruleId: "mcph-token-security/prefer-env-token",
44162
44304
  line: tokenPos.line,
44163
44305
  message: `prefer MCPH_TOKEN env var over a file-stored token in ${config2.relativePath}`,
44164
44306
  suggestion: `Delete line ${tokenPos.line} (the "token" field) and export instead:
@@ -44199,7 +44341,7 @@ async function checkMcphApibase(config2, _projectRoot) {
44199
44341
  issues.push({
44200
44342
  severity: "error",
44201
44343
  check: "mcph-apibase",
44202
- ruleId: "mcph-config/invalid-apibase",
44344
+ ruleId: "mcph-apibase/invalid-apibase",
44203
44345
  line: pos.line,
44204
44346
  message: `"apiBase" is not a valid URL: ${value}`,
44205
44347
  suggestion: `Use an absolute http(s) URL, e.g. "https://mcp.hosting".`
@@ -44210,7 +44352,7 @@ async function checkMcphApibase(config2, _projectRoot) {
44210
44352
  issues.push({
44211
44353
  severity: "warning",
44212
44354
  check: "mcph-apibase",
44213
- ruleId: "mcph-config/insecure-apibase",
44355
+ ruleId: "mcph-apibase/insecure-apibase",
44214
44356
  line: pos.line,
44215
44357
  message: `"apiBase" uses plaintext HTTP to a public host (${parsed.hostname})`,
44216
44358
  suggestion: `Use https:// instead. Plaintext HTTP exposes your MCPH_TOKEN on the wire.
@@ -44246,7 +44388,7 @@ async function checkMcphSchemaConformance(config2, _projectRoot) {
44246
44388
  issues.push({
44247
44389
  severity: "warning",
44248
44390
  check: "mcph-schema-conformance",
44249
- ruleId: "mcph-config/unknown-field",
44391
+ ruleId: "mcph-schema-conformance/unknown-field",
44250
44392
  line: field.position.line,
44251
44393
  message: `unknown field "${field.name}" \u2014 not in the mcph config schema`,
44252
44394
  suggestion: `Known fields: $schema, version, token, apiBase, servers, blocked. Check for typos (e.g. "tokens" vs "token", "blockList" vs "blocked").`
@@ -44258,7 +44400,7 @@ async function checkMcphSchemaConformance(config2, _projectRoot) {
44258
44400
  issues.push({
44259
44401
  severity: "info",
44260
44402
  check: "mcph-schema-conformance",
44261
- ruleId: "mcph-config/stale-version",
44403
+ ruleId: "mcph-schema-conformance/stale-version",
44262
44404
  line: versionPos.line,
44263
44405
  message: `"version": ${version2} is older than the current schema version (${CURRENT_SCHEMA_VERSION})`,
44264
44406
  suggestion: `Update to "version": ${CURRENT_SCHEMA_VERSION}. Older versions continue to load but may miss newer fields.`
@@ -44286,7 +44428,7 @@ async function checkMcphLists(config2, _projectRoot) {
44286
44428
  issues.push({
44287
44429
  severity: "warning",
44288
44430
  check: "mcph-lists",
44289
- ruleId: "mcph-config/allowlist-denylist-conflict",
44431
+ ruleId: "mcph-lists/allowlist-denylist-conflict",
44290
44432
  line: entry.position.line,
44291
44433
  message: `server "${entry.value}" is in both "servers" (allow-list) and "blocked" (deny-list)`,
44292
44434
  suggestion: `Remove "${entry.value}" from one of the two lists. "blocked" wins in practice (deny > allow), so the allow-list entry is dead weight.`
@@ -44302,7 +44444,7 @@ async function checkMcphLists(config2, _projectRoot) {
44302
44444
  issues.push({
44303
44445
  severity: "info",
44304
44446
  check: "mcph-lists",
44305
- ruleId: "mcph-config/duplicate-entries",
44447
+ ruleId: "mcph-lists/duplicate-entries",
44306
44448
  line: entry.position.line,
44307
44449
  message: `"${entry.value}" appears multiple times in "${listName}" (first at line ${prevLine})`,
44308
44450
  suggestion: `Remove the duplicate entry at line ${entry.position.line}.`
@@ -44329,7 +44471,7 @@ async function checkMcphGitignore(config2, _projectRoot) {
44329
44471
  issues.push({
44330
44472
  severity: "error",
44331
44473
  check: "mcph-gitignore",
44332
- ruleId: "mcph-config/local-file-not-gitignored",
44474
+ ruleId: "mcph-gitignore/local-file-not-gitignored",
44333
44475
  line: 1,
44334
44476
  message: `${basename4} is not covered by .gitignore \u2014 machine-local overrides can leak via git`,
44335
44477
  suggestion: `Add "${basename4}" to .gitignore in your project root.`
@@ -44360,6 +44502,12 @@ function extractPaths(content) {
44360
44502
  paths.push(p2);
44361
44503
  }
44362
44504
  }
44505
+ for (const match of content.matchAll(BARE_FILE_PATH)) {
44506
+ const p2 = match[1].replace(/[)}\]]+$/, "");
44507
+ if (p2.length > 2 && !p2.startsWith("http")) {
44508
+ paths.push(p2);
44509
+ }
44510
+ }
44363
44511
  return [...new Set(paths)];
44364
44512
  }
44365
44513
  function parseFrontmatter2(content) {
@@ -44389,7 +44537,7 @@ function parseFrontmatter2(content) {
44389
44537
  };
44390
44538
  }
44391
44539
  async function parseMemoryFile(filePath, projectDir) {
44392
- const content = await readFile(filePath, "utf-8");
44540
+ const content = stripBom(await readFile(filePath, "utf-8"));
44393
44541
  const { name, description, type, body } = parseFrontmatter2(content);
44394
44542
  const referencedPaths = extractPaths(body);
44395
44543
  return {
@@ -44402,11 +44550,13 @@ async function parseMemoryFile(filePath, projectDir) {
44402
44550
  referencedPaths
44403
44551
  };
44404
44552
  }
44405
- var PATH_PATTERN2;
44553
+ var PATH_PATTERN2, BARE_FILE_PATH;
44406
44554
  var init_session_parser = __esm({
44407
44555
  "src/core/session-parser.ts"() {
44408
44556
  "use strict";
44557
+ init_fs();
44409
44558
  PATH_PATTERN2 = /(?:^|\s|['"`(])([.~/][^\s'"`),;:!?]+)/g;
44559
+ BARE_FILE_PATH = /(?:^|[\s`"'(])([\w][\w-]*(?:\/[\w.-]+)+\.[a-zA-Z0-9]{1,8})\b/g;
44410
44560
  }
44411
44561
  });
44412
44562
 
@@ -44438,30 +44588,46 @@ async function parseJsonlFiltered(filePath, filter) {
44438
44588
  }
44439
44589
  return results;
44440
44590
  }
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;
44591
+ function pickField(entry, fields) {
44592
+ for (const f of fields) {
44593
+ const v2 = entry[f];
44594
+ if (typeof v2 === "string" && v2.length > 0) return v2;
44595
+ }
44596
+ return "";
44597
+ }
44598
+ async function readJsonlHistory(opts) {
44599
+ return parseJsonlFiltered(opts.historyPath, (parsed) => {
44600
+ if (!parsed || typeof parsed !== "object") return null;
44601
+ const entry = parsed;
44602
+ const display = pickField(entry, opts.displayFields);
44603
+ if (!display) return null;
44604
+ const project = pickField(entry, opts.projectFields);
44605
+ if (opts.requireProject && !project) return null;
44445
44606
  return {
44446
- display: entry.display,
44447
- timestamp: entry.timestamp || 0,
44448
- project: entry.project.replace(/\\/g, "/"),
44449
- sessionId: entry.sessionId || "",
44450
- provider: "claude-code"
44607
+ display,
44608
+ timestamp: typeof entry.timestamp === "number" ? entry.timestamp : 0,
44609
+ project: project.replace(/\\/g, "/"),
44610
+ sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
44611
+ provider: opts.provider
44451
44612
  };
44452
44613
  });
44453
44614
  }
44615
+ async function readClaudeHistory() {
44616
+ return readJsonlHistory({
44617
+ historyPath: join5(home, ".claude", "history.jsonl"),
44618
+ provider: "claude-code",
44619
+ displayFields: ["display"],
44620
+ projectFields: ["project"],
44621
+ requireProject: true
44622
+ });
44623
+ }
44454
44624
  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
- };
44625
+ return readJsonlHistory({
44626
+ historyPath: join5(home, ".codex", "history.jsonl"),
44627
+ provider: "codex-cli",
44628
+ displayFields: ["display", "command"],
44629
+ projectFields: ["project", "cwd"],
44630
+ requireProject: false
44465
44631
  });
44466
44632
  }
44467
44633
  async function readClaudeMemories() {
@@ -44526,47 +44692,29 @@ async function detectSiblings(projectRoot) {
44526
44692
  if (orgMatch) currentOrg = orgMatch[1];
44527
44693
  } catch {
44528
44694
  }
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
- );
44695
+ const results = await Promise.all(candidates.map((c3) => resolveSibling(c3)));
44547
44696
  if (currentOrg) {
44548
- return results.filter((s) => s.gitOrg === currentOrg);
44697
+ return results.filter((s) => !s.gitOrg || s.gitOrg === currentOrg);
44549
44698
  }
44550
44699
  return results;
44551
44700
  }
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;
44701
+ return Promise.all(candidates.map((c3) => resolveSibling(c3)));
44702
+ }
44703
+ async function resolveSibling(c3) {
44704
+ const sibling = { path: c3.entryPath.replace(/\\/g, "/"), name: c3.name };
44705
+ if (!existsSync(join5(c3.fullPath, ".git"))) return sibling;
44706
+ try {
44707
+ const git = simpleGit(c3.fullPath);
44708
+ const remotes = await git.getRemotes(true);
44709
+ const origin = remotes.find((r2) => r2.name === "origin");
44710
+ if (origin?.refs?.fetch) {
44711
+ sibling.gitRemoteUrl = origin.refs.fetch;
44712
+ const orgMatch = origin.refs.fetch.match(/github\.com[:/]([^/]+)\//);
44713
+ if (orgMatch) sibling.gitOrg = orgMatch[1];
44714
+ }
44715
+ } catch {
44716
+ }
44717
+ return sibling;
44570
44718
  }
44571
44719
  async function scanSessionData(projectRoot) {
44572
44720
  const providers = detectProviders();
@@ -44666,7 +44814,7 @@ async function checkMissingSecret(ctx) {
44666
44814
  issues.push({
44667
44815
  severity: "error",
44668
44816
  check: "session-missing-secret",
44669
- ruleId: "session/missing-secret",
44817
+ ruleId: "session-missing-secret/missing-secret",
44670
44818
  line: 0,
44671
44819
  message: `GitHub secret "${secretName}" is set on ${siblingMatches.length} sibling repos (${sibNames}) but not on this project`,
44672
44820
  suggestion: `Run: gh secret set ${secretName} --repo <owner>/<repo>`,
@@ -44686,48 +44834,57 @@ var init_missing_secret = __esm({
44686
44834
  });
44687
44835
 
44688
44836
  // src/core/checks/session/diverged-file.ts
44689
- import { readFile as readFile2 } from "node:fs/promises";
44837
+ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
44690
44838
  import { join as join6 } from "node:path";
44691
44839
  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++;
44840
+ async function loadLineSet(absPath) {
44841
+ let mtimeMs;
44842
+ let size;
44843
+ try {
44844
+ const stats = await stat2(absPath);
44845
+ mtimeMs = stats.mtimeMs;
44846
+ size = stats.size;
44847
+ } catch {
44848
+ return null;
44704
44849
  }
44705
- const unionSize = linesA.size + linesB.size - intersection2;
44706
- return intersection2 / unionSize;
44850
+ const cached2 = lineSetCache.get(absPath);
44851
+ if (cached2 && cached2.mtimeMs === mtimeMs && cached2.size === size) {
44852
+ lineSetCache.delete(absPath);
44853
+ lineSetCache.set(absPath, cached2);
44854
+ return cached2.lineSet;
44855
+ }
44856
+ let content;
44857
+ try {
44858
+ content = stripBom(await readFile2(absPath, "utf-8"));
44859
+ } catch {
44860
+ return null;
44861
+ }
44862
+ const lineSet = toLineSet(content, MIN_TOKEN_LEN);
44863
+ lineSetCache.set(absPath, { mtimeMs, size, lineSet });
44864
+ if (lineSetCache.size > CACHE_MAX_ENTRIES) {
44865
+ const oldest = lineSetCache.keys().next().value;
44866
+ if (oldest !== void 0) lineSetCache.delete(oldest);
44867
+ }
44868
+ return lineSet;
44707
44869
  }
44708
44870
  async function checkDivergedFile(ctx) {
44709
44871
  const issues = [];
44710
44872
  for (const fileName of CANONICAL_FILES) {
44711
44873
  const currentPath = join6(ctx.currentProject, fileName);
44712
44874
  if (!existsSync2(currentPath)) continue;
44713
- let currentContent;
44714
- try {
44715
- currentContent = await readFile2(currentPath, "utf-8");
44716
- } catch {
44717
- continue;
44718
- }
44875
+ const currentLineSet = await loadLineSet(currentPath);
44876
+ if (!currentLineSet) continue;
44719
44877
  const diverged = [];
44720
44878
  for (const sib of ctx.siblings) {
44721
44879
  const sibPath = join6(sib.path, fileName);
44722
44880
  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;
44881
+ const sibLineSet = await loadLineSet(sibPath);
44882
+ if (!sibLineSet) continue;
44883
+ const overlap = jaccardSimilarityFromSets(currentLineSet, sibLineSet, {
44884
+ bothEmptyIsIdentical: true
44885
+ });
44886
+ if (overlap >= 0.2 && overlap < 0.9) {
44887
+ diverged.push({ sibling: sib.name, overlap: Math.round(overlap * 100) });
44731
44888
  }
44732
44889
  }
44733
44890
  if (diverged.length > 0) {
@@ -44735,7 +44892,7 @@ async function checkDivergedFile(ctx) {
44735
44892
  issues.push({
44736
44893
  severity: "warning",
44737
44894
  check: "session-diverged-file",
44738
- ruleId: "session/diverged-file",
44895
+ ruleId: "session-diverged-file/diverged-file",
44739
44896
  line: 0,
44740
44897
  message: `${fileName} has diverged from sibling repos: ${details}`,
44741
44898
  suggestion: `Compare with sibling versions to identify unintentional drift`,
@@ -44745,10 +44902,12 @@ async function checkDivergedFile(ctx) {
44745
44902
  }
44746
44903
  return issues;
44747
44904
  }
44748
- var CANONICAL_FILES;
44905
+ var CANONICAL_FILES, MIN_TOKEN_LEN, CACHE_MAX_ENTRIES, lineSetCache;
44749
44906
  var init_diverged_file = __esm({
44750
44907
  "src/core/checks/session/diverged-file.ts"() {
44751
44908
  "use strict";
44909
+ init_similarity();
44910
+ init_fs();
44752
44911
  CANONICAL_FILES = [
44753
44912
  "release.sh",
44754
44913
  ".github/workflows/ci.yml",
@@ -44759,6 +44918,9 @@ var init_diverged_file = __esm({
44759
44918
  "tsconfig.json",
44760
44919
  ".gitignore"
44761
44920
  ];
44921
+ MIN_TOKEN_LEN = 3;
44922
+ CACHE_MAX_ENTRIES = 256;
44923
+ lineSetCache = /* @__PURE__ */ new Map();
44762
44924
  }
44763
44925
  });
44764
44926
 
@@ -44802,7 +44964,7 @@ async function checkMissingWorkflow(ctx) {
44802
44964
  issues.push({
44803
44965
  severity: "warning",
44804
44966
  check: "session-missing-workflow",
44805
- ruleId: "session/missing-workflow",
44967
+ ruleId: "session-missing-workflow/missing-workflow",
44806
44968
  line: 0,
44807
44969
  message: `GitHub Actions workflow "${workflow}" exists in ${siblings.length} sibling repos (${sibNames}) but not in this project`,
44808
44970
  suggestion: `Consider adding .github/workflows/${workflow} for consistency`,
@@ -44848,7 +45010,7 @@ async function checkStaleMemory(ctx) {
44848
45010
  issues.push({
44849
45011
  severity: "info",
44850
45012
  check: "session-stale-memory",
44851
- ruleId: "session/stale-memory",
45013
+ ruleId: "session-stale-memory/stale-memory",
44852
45014
  line: 0,
44853
45015
  message: `Memory "${name}" references ${brokenPaths.length} path(s) that no longer exist: ${brokenPaths.join(", ")}`,
44854
45016
  suggestion: `Update or remove the memory file: ${mem.filePath}`,
@@ -44866,24 +45028,10 @@ var init_stale_memory = __esm({
44866
45028
  });
44867
45029
 
44868
45030
  // 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
45031
  async function checkDuplicateMemory(ctx) {
44885
45032
  const issues = [];
44886
45033
  const reported = /* @__PURE__ */ new Set();
45034
+ const lineSets = ctx.memories.map((m) => toLineSet(m.content, MIN_TOKEN_LEN2));
44887
45035
  for (let i2 = 0; i2 < ctx.memories.length; i2++) {
44888
45036
  for (let j3 = i2 + 1; j3 < ctx.memories.length; j3++) {
44889
45037
  const a = ctx.memories[i2];
@@ -44893,7 +45041,7 @@ async function checkDuplicateMemory(ctx) {
44893
45041
  const bIsCurrent = projectDirMatchesPath(b2.projectDir, ctx.currentProject);
44894
45042
  if (!aIsCurrent && !bIsCurrent) continue;
44895
45043
  if (a.content.length < 50 || b2.content.length < 50) continue;
44896
- const overlap = calculateLineOverlap2(a.content, b2.content);
45044
+ const overlap = jaccardSimilarityFromSets(lineSets[i2], lineSets[j3]);
44897
45045
  if (overlap < 0.6) continue;
44898
45046
  const pairKey = [a.filePath, b2.filePath].sort().join("::");
44899
45047
  if (reported.has(pairKey)) continue;
@@ -44905,7 +45053,7 @@ async function checkDuplicateMemory(ctx) {
44905
45053
  issues.push({
44906
45054
  severity: "info",
44907
45055
  check: "session-duplicate-memory",
44908
- ruleId: "session/duplicate-memory",
45056
+ ruleId: "session-duplicate-memory/duplicate-memory",
44909
45057
  line: 0,
44910
45058
  message: `Memory "${nameA}" (${projA}) and "${nameB}" (${projB}) have ${Math.round(overlap * 100)}% overlap`,
44911
45059
  suggestion: `Consider consolidating into a shared memory or removing the duplicate`,
@@ -44915,10 +45063,13 @@ async function checkDuplicateMemory(ctx) {
44915
45063
  }
44916
45064
  return issues;
44917
45065
  }
45066
+ var MIN_TOKEN_LEN2;
44918
45067
  var init_duplicate_memory = __esm({
44919
45068
  "src/core/checks/session/duplicate-memory.ts"() {
44920
45069
  "use strict";
44921
45070
  init_session_parser();
45071
+ init_similarity();
45072
+ MIN_TOKEN_LEN2 = 5;
44922
45073
  }
44923
45074
  });
44924
45075
 
@@ -44973,7 +45124,8 @@ function findCyclicPatterns(displays) {
44973
45124
  async function checkLoopDetection(ctx) {
44974
45125
  const issues = [];
44975
45126
  const currentNorm = normalizeProject(ctx.currentProject);
44976
- const entries = ctx.history.filter((e) => normalizeProject(e.project) === currentNorm).sort((a, b2) => a.timestamp - b2.timestamp);
45127
+ const filtered = ctx.history.filter((e) => normalizeProject(e.project) === currentNorm).sort((a, b2) => a.timestamp - b2.timestamp);
45128
+ const entries = filtered.length > MAX_HISTORY_ENTRIES ? filtered.slice(-MAX_HISTORY_ENTRIES) : filtered;
44977
45129
  if (entries.length < CONSECUTIVE_THRESHOLD) return issues;
44978
45130
  const displays = entries.map((e) => e.display);
44979
45131
  const repeats = findConsecutiveRepeats(displays);
@@ -44982,7 +45134,7 @@ async function checkLoopDetection(ctx) {
44982
45134
  issues.push({
44983
45135
  severity: "warning",
44984
45136
  check: "session-loop-detection",
44985
- ruleId: "session/consecutive-repeat",
45137
+ ruleId: "session-loop-detection/consecutive-repeat",
44986
45138
  line: 0,
44987
45139
  message: `Command run ${count} times consecutively: "${truncated}"`,
44988
45140
  suggestion: "An agent may be looping on this command. Check history.jsonl for context on what went wrong"
@@ -44994,7 +45146,7 @@ async function checkLoopDetection(ctx) {
44994
45146
  issues.push({
44995
45147
  severity: "warning",
44996
45148
  check: "session-loop-detection",
44997
- ruleId: "session/cyclic-pattern",
45149
+ ruleId: "session-loop-detection/cyclic-pattern",
44998
45150
  line: 0,
44999
45151
  message: `Cyclic pattern repeated ${reps} times: ${cycleStr}`,
45000
45152
  suggestion: "An agent may be stuck in a loop. Check if a context file is missing instructions for this workflow"
@@ -45002,42 +45154,42 @@ async function checkLoopDetection(ctx) {
45002
45154
  }
45003
45155
  return issues;
45004
45156
  }
45005
- var CONSECUTIVE_THRESHOLD, CYCLE_REPEAT_THRESHOLD, MAX_CYCLE_LENGTH;
45157
+ var CONSECUTIVE_THRESHOLD, CYCLE_REPEAT_THRESHOLD, MAX_CYCLE_LENGTH, MAX_HISTORY_ENTRIES;
45006
45158
  var init_loop_detection = __esm({
45007
45159
  "src/core/checks/session/loop-detection.ts"() {
45008
45160
  "use strict";
45009
45161
  CONSECUTIVE_THRESHOLD = 3;
45010
45162
  CYCLE_REPEAT_THRESHOLD = 2;
45011
45163
  MAX_CYCLE_LENGTH = 3;
45164
+ MAX_HISTORY_ENTRIES = 5e3;
45012
45165
  }
45013
45166
  });
45014
45167
 
45015
45168
  // src/core/checks/session/memory-index-overflow.ts
45016
- import { readFile as readFile3, stat as stat2 } from "node:fs/promises";
45169
+ import { readFile as readFile3 } from "node:fs/promises";
45017
45170
  import { join as join8 } from "node:path";
45018
45171
  async function checkMemoryIndexOverflow(ctx) {
45019
45172
  const home2 = process.env.HOME || process.env.USERPROFILE || "";
45020
45173
  if (!home2) return [];
45021
45174
  const encoded = encodeProjectDir(ctx.currentProject);
45022
45175
  const memoryFile = join8(home2, ".claude", "projects", encoded, "memory", "MEMORY.md");
45023
- let stats;
45176
+ let content;
45024
45177
  try {
45025
- stats = await stat2(memoryFile);
45178
+ content = stripBom(await readFile3(memoryFile, "utf-8"));
45026
45179
  } catch {
45027
45180
  return [];
45028
45181
  }
45029
- const content = await readFile3(memoryFile, "utf-8").catch(() => "");
45030
45182
  if (!content) return [];
45031
45183
  const lines = content.split("\n");
45032
45184
  const lineCount = lines.length;
45033
- const byteSize = stats.size;
45185
+ const byteSize = Buffer.byteLength(content, "utf8");
45034
45186
  const issues = [];
45035
45187
  if (lineCount > MAX_LINES) {
45036
45188
  const excess = lineCount - MAX_LINES;
45037
45189
  issues.push({
45038
45190
  severity: "warning",
45039
45191
  check: "session-memory-index-overflow",
45040
- ruleId: "session/memory-index-overflow",
45192
+ ruleId: "session-memory-index-overflow/memory-index-overflow",
45041
45193
  line: MAX_LINES + 1,
45042
45194
  message: `MEMORY.md has ${lineCount.toLocaleString()} lines \u2014 only the first ${MAX_LINES} are loaded. ${excess.toLocaleString()} line(s) are effectively invisible to the agent.`,
45043
45195
  detail: `File: ${memoryFile}`,
@@ -45049,7 +45201,7 @@ async function checkMemoryIndexOverflow(ctx) {
45049
45201
  issues.push({
45050
45202
  severity: "warning",
45051
45203
  check: "session-memory-index-overflow",
45052
- ruleId: "session/memory-index-overflow",
45204
+ ruleId: "session-memory-index-overflow/memory-index-overflow",
45053
45205
  line: 0,
45054
45206
  message: `MEMORY.md is ${byteSize.toLocaleString()} bytes \u2014 only the first ${MAX_BYTES.toLocaleString()} bytes are loaded. ~${excess.toLocaleString()} bytes are effectively invisible.`,
45055
45207
  detail: `File: ${memoryFile}`,
@@ -45063,6 +45215,7 @@ var init_memory_index_overflow = __esm({
45063
45215
  "src/core/checks/session/memory-index-overflow.ts"() {
45064
45216
  "use strict";
45065
45217
  init_session_parser();
45218
+ init_fs();
45066
45219
  MAX_LINES = 200;
45067
45220
  MAX_BYTES = 25 * 1024;
45068
45221
  }
@@ -45089,7 +45242,7 @@ async function findReleaseWorkflows(projectRoot) {
45089
45242
  continue;
45090
45243
  }
45091
45244
  try {
45092
- const content = await readFile4(join9(workflowDir, f), "utf-8");
45245
+ const content = stripBom(await readFile4(join9(workflowDir, f), "utf-8"));
45093
45246
  const nameMatch = content.match(/^name:\s*(.+)$/m);
45094
45247
  if (nameMatch && RELEASE_FILENAME_PATTERNS.some((p2) => p2.test(nameMatch[1]))) {
45095
45248
  releaseWorkflows.push(f);
@@ -45115,7 +45268,7 @@ async function checkCiCoverage(files, projectRoot) {
45115
45268
  {
45116
45269
  severity: "info",
45117
45270
  check: "ci-coverage",
45118
- ruleId: "ci/no-release-docs",
45271
+ ruleId: "ci-coverage/no-release-docs",
45119
45272
  line: 0,
45120
45273
  message: `Release workflow${releaseWorkflows.length > 1 ? "s" : ""} found (${releaseWorkflows.join(", ")}) but no context file documents the release process`,
45121
45274
  suggestion: `Document how releases work (e.g. "push a v* tag to trigger CI") in a context file so agents don't guess`
@@ -45126,6 +45279,7 @@ var RELEASE_FILENAME_PATTERNS, RELEASE_DOC_PATTERNS;
45126
45279
  var init_ci_coverage = __esm({
45127
45280
  "src/core/checks/ci-coverage.ts"() {
45128
45281
  "use strict";
45282
+ init_fs();
45129
45283
  RELEASE_FILENAME_PATTERNS = [/release/i, /deploy/i, /publish/i, /\bcd\b/i];
45130
45284
  RELEASE_DOC_PATTERNS = [
45131
45285
  /release\s+(process|workflow|steps|via|by|using)/i,
@@ -45160,7 +45314,7 @@ async function findSecretUsages(projectRoot) {
45160
45314
  if (!(f.endsWith(".yml") || f.endsWith(".yaml"))) continue;
45161
45315
  let content;
45162
45316
  try {
45163
- content = await readFile5(join10(workflowDir, f), "utf-8");
45317
+ content = stripBom(await readFile5(join10(workflowDir, f), "utf-8"));
45164
45318
  } catch {
45165
45319
  continue;
45166
45320
  }
@@ -45177,7 +45331,7 @@ async function findSecretUsages(projectRoot) {
45177
45331
  }
45178
45332
  function contextMentionsSecret(files, secretName) {
45179
45333
  const escaped = secretName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
45180
- const pattern = new RegExp(`\\b${escaped.replace(/_/g, "[_\\s-]")}\\b`, "i");
45334
+ const pattern = new RegExp(`\\b${escaped.replace(/_/g, "[ _-]")}\\b`, "i");
45181
45335
  return files.some((f) => pattern.test(f.content));
45182
45336
  }
45183
45337
  async function checkCiSecrets(files, projectRoot) {
@@ -45189,7 +45343,7 @@ async function checkCiSecrets(files, projectRoot) {
45189
45343
  issues.push({
45190
45344
  severity: "info",
45191
45345
  check: "ci-secrets",
45192
- ruleId: "ci/undocumented-secret",
45346
+ ruleId: "ci-secrets/undocumented-secret",
45193
45347
  line: 0,
45194
45348
  message: `CI secret "${name}" is used in ${workflows.join(", ")} but not mentioned in any context file`,
45195
45349
  suggestion: `Document what ${name} is and how to set it (e.g. "gh secret set ${name}") so agents don't create new tokens or guess`
@@ -45201,6 +45355,7 @@ var BUILTIN_SECRETS, SECRETS_REGEX;
45201
45355
  var init_ci_secrets = __esm({
45202
45356
  "src/core/checks/ci-secrets.ts"() {
45203
45357
  "use strict";
45358
+ init_fs();
45204
45359
  BUILTIN_SECRETS = /* @__PURE__ */ new Set([
45205
45360
  "GITHUB_TOKEN",
45206
45361
  "ACTIONS_RUNTIME_TOKEN",
@@ -45216,15 +45371,239 @@ var init_ci_secrets = __esm({
45216
45371
  }
45217
45372
  });
45218
45373
 
45374
+ // src/core/checks/content-secrets.ts
45375
+ function lineLooksLikePlaceholder(line) {
45376
+ const lower = line.toLowerCase();
45377
+ for (const tok of PLACEHOLDER_TOKENS) {
45378
+ if (lower.includes(tok)) return true;
45379
+ }
45380
+ return false;
45381
+ }
45382
+ function isCommentedExample(line) {
45383
+ if (!COMMENT_PREFIX.test(line)) return false;
45384
+ const lower = line.toLowerCase();
45385
+ return lower.includes("fake") || lower.includes("example");
45386
+ }
45387
+ function isPlaceholderWrapped(line, start, end) {
45388
+ const before2 = line.slice(Math.max(0, start - 2), start);
45389
+ const after2 = line.slice(end, end + 2);
45390
+ if (before2.endsWith("<") && after2.startsWith(">")) return true;
45391
+ if (before2.endsWith("${") && after2.startsWith("}")) return true;
45392
+ if (before2.endsWith("{") && after2.startsWith("}")) return true;
45393
+ const dollarOpen = line.lastIndexOf("${", start);
45394
+ if (dollarOpen !== -1) {
45395
+ const close = line.indexOf("}", dollarOpen);
45396
+ if (close !== -1 && close >= end) return true;
45397
+ }
45398
+ const angleOpen = line.lastIndexOf("<", start);
45399
+ if (angleOpen !== -1) {
45400
+ const close = line.indexOf(">", angleOpen);
45401
+ if (close !== -1 && close >= end) return true;
45402
+ }
45403
+ return false;
45404
+ }
45405
+ function computeFenceLanguages(lines) {
45406
+ const out = new Array(lines.length).fill(null);
45407
+ let activeMarker = null;
45408
+ let activeLang = null;
45409
+ for (let i2 = 0; i2 < lines.length; i2++) {
45410
+ const line = lines[i2];
45411
+ const trimmed2 = line.trim();
45412
+ if (activeMarker === null) {
45413
+ const open = trimmed2.match(/^(```+|~~~+)(.*)$/);
45414
+ if (open) {
45415
+ activeMarker = open[1][0];
45416
+ activeLang = open[2].trim().toLowerCase();
45417
+ continue;
45418
+ }
45419
+ } else {
45420
+ out[i2] = activeLang;
45421
+ if (trimmed2.startsWith(activeMarker.repeat(3)) && trimmed2.replace(new RegExp(`^${activeMarker === "`" ? "`" : "~"}+`), "").trim() === "") {
45422
+ activeMarker = null;
45423
+ activeLang = null;
45424
+ }
45425
+ }
45426
+ }
45427
+ return out;
45428
+ }
45429
+ function redactedPrefix(value) {
45430
+ const head = value.slice(0, 6);
45431
+ return `${head}...`;
45432
+ }
45433
+ async function checkContentSecrets(file2, _projectRoot) {
45434
+ const issues = [];
45435
+ const lines = file2.content.split(/\r?\n/);
45436
+ const fenceLangs = computeFenceLanguages(lines);
45437
+ for (let i2 = 0; i2 < lines.length; i2++) {
45438
+ const line = lines[i2];
45439
+ const lineNo = i2 + 1;
45440
+ if (isCommentedExample(line)) continue;
45441
+ const fenceLang = fenceLangs[i2];
45442
+ if (fenceLang !== null && ILLUSTRATIVE_FENCES.has(fenceLang)) continue;
45443
+ if (lineLooksLikePlaceholder(line)) continue;
45444
+ if (PRIVATE_KEY_HEADER.test(line)) {
45445
+ issues.push({
45446
+ severity: "error",
45447
+ check: "content-secrets",
45448
+ ruleId: "content-secrets/private-key-header",
45449
+ line: lineNo,
45450
+ message: `Private key header detected in ${file2.relativePath}`,
45451
+ suggestion: "Move the secret to a `.env` or secret manager and reference it by name. If this token is real, rotate it immediately."
45452
+ });
45453
+ continue;
45454
+ }
45455
+ const seen = /* @__PURE__ */ new Set();
45456
+ for (const pattern of PATTERNS) {
45457
+ const re2 = new RegExp(pattern.regex.source, pattern.regex.flags);
45458
+ let m;
45459
+ while ((m = re2.exec(line)) !== null) {
45460
+ const matched = m[0];
45461
+ const start = m.index;
45462
+ const end = start + matched.length;
45463
+ const key = `${start}:${pattern.ruleSlug}`;
45464
+ if (seen.has(key)) continue;
45465
+ let overlap = false;
45466
+ for (const k3 of seen) {
45467
+ const [s] = k3.split(":");
45468
+ if (parseInt(s, 10) === start) {
45469
+ overlap = true;
45470
+ break;
45471
+ }
45472
+ }
45473
+ if (overlap) continue;
45474
+ if (isPlaceholderWrapped(line, start, end)) continue;
45475
+ seen.add(key);
45476
+ issues.push({
45477
+ severity: "error",
45478
+ check: "content-secrets",
45479
+ ruleId: `content-secrets/${pattern.ruleSlug}`,
45480
+ line: lineNo,
45481
+ message: `${pattern.label} detected in ${file2.relativePath} (${redactedPrefix(matched)})`,
45482
+ suggestion: "Move the secret to a `.env` or secret manager and reference it by name. If this token is real, rotate it immediately."
45483
+ });
45484
+ }
45485
+ }
45486
+ }
45487
+ return issues;
45488
+ }
45489
+ var PATTERNS, PRIVATE_KEY_HEADER, PLACEHOLDER_TOKENS, COMMENT_PREFIX, ILLUSTRATIVE_FENCES;
45490
+ var init_content_secrets = __esm({
45491
+ "src/core/checks/content-secrets.ts"() {
45492
+ "use strict";
45493
+ PATTERNS = [
45494
+ // AWS access key (long-lived) -- AKIA prefix + 16 uppercase alphanum chars.
45495
+ {
45496
+ ruleSlug: "aws-access-key",
45497
+ label: "AWS access key",
45498
+ regex: /\bAKIA[0-9A-Z]{16}\b/g
45499
+ },
45500
+ // AWS STS temporary access key -- ASIA prefix.
45501
+ {
45502
+ ruleSlug: "aws-access-key",
45503
+ label: "AWS access key",
45504
+ regex: /\bASIA[0-9A-Z]{16}\b/g
45505
+ },
45506
+ // GitHub classic PAT.
45507
+ {
45508
+ ruleSlug: "github-pat",
45509
+ label: "GitHub personal access token",
45510
+ regex: /\bghp_[A-Za-z0-9]{36,}\b/g
45511
+ },
45512
+ // GitHub fine-grained PAT.
45513
+ {
45514
+ ruleSlug: "github-pat",
45515
+ label: "GitHub personal access token",
45516
+ regex: /\bgithub_pat_[A-Za-z0-9_]{82,}\b/g
45517
+ },
45518
+ // GitHub server-to-server / OAuth / user / refresh tokens.
45519
+ {
45520
+ ruleSlug: "github-pat",
45521
+ label: "GitHub token",
45522
+ regex: /\bghs_[A-Za-z0-9]{36,}\b/g
45523
+ },
45524
+ {
45525
+ ruleSlug: "github-pat",
45526
+ label: "GitHub token",
45527
+ regex: /\bgho_[A-Za-z0-9]{36,}\b/g
45528
+ },
45529
+ {
45530
+ ruleSlug: "github-pat",
45531
+ label: "GitHub token",
45532
+ regex: /\bghu_[A-Za-z0-9]{36,}\b/g
45533
+ },
45534
+ {
45535
+ ruleSlug: "github-pat",
45536
+ label: "GitHub token",
45537
+ regex: /\bghr_[A-Za-z0-9]{36,}\b/g
45538
+ },
45539
+ // Anthropic API keys.
45540
+ {
45541
+ ruleSlug: "anthropic-key",
45542
+ label: "Anthropic API key",
45543
+ regex: /\bsk-ant-[A-Za-z0-9\-_]{20,}\b/g
45544
+ },
45545
+ // OpenAI API keys (project-scoped or classic). Match sk- or sk-proj- with at
45546
+ // least 20 random chars to avoid catching `sk-...` ellipses in docs.
45547
+ {
45548
+ ruleSlug: "openai-key",
45549
+ label: "OpenAI API key",
45550
+ regex: /\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}\b/g
45551
+ },
45552
+ // npm automation tokens.
45553
+ {
45554
+ ruleSlug: "npm-token",
45555
+ label: "npm token",
45556
+ regex: /\bnpm_[A-Za-z0-9]{36,}\b/g
45557
+ },
45558
+ // Slack tokens (bot, user, app, admin, refresh).
45559
+ {
45560
+ ruleSlug: "slack-token",
45561
+ label: "Slack token",
45562
+ regex: /\bxox[bpoasr]-[A-Za-z0-9\-]{10,}\b/g
45563
+ },
45564
+ // mcp.hosting PAT -- consistent with mcph/token-security.ts.
45565
+ {
45566
+ ruleSlug: "mcph-pat",
45567
+ label: "mcp.hosting PAT",
45568
+ regex: /\bmcp_pat_[A-Za-z0-9]{32,}\b/g
45569
+ },
45570
+ // Google API keys.
45571
+ {
45572
+ ruleSlug: "google-api-key",
45573
+ label: "Google API key",
45574
+ regex: /\bAIza[0-9A-Za-z\-_]{35}\b/g
45575
+ },
45576
+ // Stripe live secret keys.
45577
+ {
45578
+ ruleSlug: "stripe-secret",
45579
+ label: "Stripe live secret key",
45580
+ regex: /\bsk_live_[0-9a-zA-Z]{24,}\b/g
45581
+ }
45582
+ ];
45583
+ PRIVATE_KEY_HEADER = /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/;
45584
+ PLACEHOLDER_TOKENS = [
45585
+ "example",
45586
+ "placeholder",
45587
+ "your-key",
45588
+ "<replace",
45589
+ "redacted",
45590
+ "xxxx",
45591
+ "****"
45592
+ ];
45593
+ COMMENT_PREFIX = /^\s*(?:#|\/\/|--|<!--)/;
45594
+ ILLUSTRATIVE_FENCES = /* @__PURE__ */ new Set(["text", "txt", "example", "pseudocode", "none", ""]);
45595
+ }
45596
+ });
45597
+
45219
45598
  // src/version.ts
45220
- import { readFileSync as readFileSync4 } from "node:fs";
45599
+ import { readFileSync as readFileSync5 } from "node:fs";
45221
45600
  import { resolve as resolve8, dirname as dirname3 } from "node:path";
45222
45601
  import { fileURLToPath } from "node:url";
45223
45602
  function loadVersion() {
45224
- if (true) return "0.9.19";
45603
+ if (true) return "0.10.0";
45225
45604
  const __dir = dirname3(fileURLToPath(import.meta.url));
45226
45605
  const pkgPath = resolve8(__dir, "../package.json");
45227
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
45606
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
45228
45607
  return pkg.version;
45229
45608
  }
45230
45609
  var VERSION;
@@ -45245,8 +45624,14 @@ function hasMcphChecks(checks) {
45245
45624
  function hasSessionChecks(checks) {
45246
45625
  return checks.some((c3) => c3.startsWith("session-"));
45247
45626
  }
45627
+ function deriveChecksToRun(activeChecks, prefix, enabled, allChecks) {
45628
+ const filtered = activeChecks.filter((c3) => c3.startsWith(prefix));
45629
+ if (filtered.length > 0) return filtered;
45630
+ return enabled ? [...allChecks] : [];
45631
+ }
45248
45632
  async function runAudit(projectRoot, activeChecks, options = {}) {
45249
45633
  const fileResults = [];
45634
+ const thresholds = resolveTokenThresholds(options.tokenThresholds);
45250
45635
  const shouldRunContextChecks = !options.mcpOnly && !options.mcphOnly && !options.sessionOnly;
45251
45636
  const shouldRunMcpChecks = options.mcp || options.mcpGlobal || options.mcpOnly || hasMcpChecks(activeChecks);
45252
45637
  const shouldRunMcphChecks = options.mcph || options.mcphGlobal || options.mcphOnly || hasMcphChecks(activeChecks);
@@ -45262,13 +45647,16 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45262
45647
  if (activeChecks.includes("paths")) checkPromises.push(checkPaths(file2, projectRoot));
45263
45648
  if (activeChecks.includes("commands")) checkPromises.push(checkCommands(file2, projectRoot));
45264
45649
  if (activeChecks.includes("staleness")) checkPromises.push(checkStaleness(file2, projectRoot));
45265
- if (activeChecks.includes("tokens")) checkPromises.push(checkTokens(file2, projectRoot));
45650
+ if (activeChecks.includes("tokens"))
45651
+ checkPromises.push(checkTokens(file2, projectRoot, thresholds));
45266
45652
  if (activeChecks.includes("tier-tokens"))
45267
- checkPromises.push(checkTierTokens(file2, projectRoot));
45653
+ checkPromises.push(checkTierTokens(file2, projectRoot, thresholds));
45268
45654
  if (activeChecks.includes("redundancy"))
45269
45655
  checkPromises.push(checkRedundancy(file2, projectRoot));
45270
45656
  if (activeChecks.includes("frontmatter"))
45271
45657
  checkPromises.push(checkFrontmatter(file2, projectRoot));
45658
+ if (activeChecks.includes("content-secrets"))
45659
+ checkPromises.push(checkContentSecrets(file2, projectRoot));
45272
45660
  const results = await Promise.all(checkPromises);
45273
45661
  const issues = results.flat();
45274
45662
  fileResults.push({
@@ -45283,12 +45671,13 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45283
45671
  const crossFileIssues = [];
45284
45672
  if (activeChecks.includes("tokens")) {
45285
45673
  const aggIssue = checkAggregateTokens(
45286
- fileResults.map((f) => ({ path: f.path, tokens: f.tokens }))
45674
+ fileResults.map((f) => ({ path: f.path, tokens: f.tokens })),
45675
+ thresholds
45287
45676
  );
45288
45677
  if (aggIssue) crossFileIssues.push(aggIssue);
45289
45678
  }
45290
45679
  if (activeChecks.includes("tier-tokens")) {
45291
- const tierAgg = checkAggregateTierTokens(parsed);
45680
+ const tierAgg = checkAggregateTierTokens(parsed, thresholds);
45292
45681
  if (tierAgg) crossFileIssues.push(tierAgg);
45293
45682
  }
45294
45683
  if (activeChecks.includes("redundancy")) {
@@ -45325,8 +45714,12 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45325
45714
  );
45326
45715
  mcpConfigs.push(...globalConfigs);
45327
45716
  }
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 : [];
45717
+ const mcpChecksToRun = deriveChecksToRun(
45718
+ activeChecks,
45719
+ "mcp-",
45720
+ Boolean(options.mcp || options.mcpGlobal || options.mcpOnly),
45721
+ ALL_MCP_CHECKS
45722
+ );
45330
45723
  for (const config2 of mcpConfigs) {
45331
45724
  const checkPromises = [];
45332
45725
  if (mcpChecksToRun.includes("mcp-schema"))
@@ -45372,7 +45765,7 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45372
45765
  const projectMcphFiles = await scanForMcphConfigs(projectRoot);
45373
45766
  const mcphConfigs = await Promise.all(
45374
45767
  projectMcphFiles.map(
45375
- (f) => parseMchpConfig(
45768
+ (f) => parseMcphConfig(
45376
45769
  f,
45377
45770
  projectRoot,
45378
45771
  f.relativePath.endsWith(".mcph.local.json") ? "project-local" : "project"
@@ -45382,12 +45775,16 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45382
45775
  if (options.mcphGlobal) {
45383
45776
  const globalMcphFiles = await scanGlobalMcphConfigs();
45384
45777
  const globalMcphConfigs = await Promise.all(
45385
- globalMcphFiles.map((f) => parseMchpConfig(f, projectRoot, "global"))
45778
+ globalMcphFiles.map((f) => parseMcphConfig(f, projectRoot, "global"))
45386
45779
  );
45387
45780
  mcphConfigs.push(...globalMcphConfigs);
45388
45781
  }
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 : [];
45782
+ const mcphChecksToRun = deriveChecksToRun(
45783
+ activeChecks,
45784
+ "mcph-",
45785
+ Boolean(options.mcph || options.mcphGlobal || options.mcphOnly),
45786
+ ALL_MCPH_CHECKS
45787
+ );
45391
45788
  for (const config2 of mcphConfigs) {
45392
45789
  const checkPromises = [];
45393
45790
  if (mcphChecksToRun.includes("mcph-token-security")) {
@@ -45411,14 +45808,16 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45411
45808
  }
45412
45809
  const results = await Promise.all(checkPromises);
45413
45810
  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
- });
45811
+ if (mcphChecksToRun.includes("mcph-schema-conformance")) {
45812
+ for (const err of config2.parseErrors) {
45813
+ issues.push({
45814
+ severity: "error",
45815
+ check: "mcph-schema-conformance",
45816
+ ruleId: "mcph-schema-conformance/parse-error",
45817
+ line: 1,
45818
+ message: err
45819
+ });
45820
+ }
45422
45821
  }
45423
45822
  const lines = config2.content.split("\n").length;
45424
45823
  fileResults.push({
@@ -45454,15 +45853,13 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45454
45853
  sessionPromises.push(checkMemoryIndexOverflow(sessionCtx));
45455
45854
  const sessionResults = await Promise.all(sessionPromises);
45456
45855
  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
- }
45856
+ fileResults.push({
45857
+ path: "~/.claude/ (session audit)",
45858
+ isSymlink: false,
45859
+ tokens: 0,
45860
+ lines: 0,
45861
+ issues: sessionIssues
45862
+ });
45466
45863
  }
45467
45864
  }
45468
45865
  let estimatedWaste = 0;
@@ -45537,6 +45934,7 @@ var init_audit = __esm({
45537
45934
  init_memory_index_overflow();
45538
45935
  init_ci_coverage();
45539
45936
  init_ci_secrets();
45937
+ init_content_secrets();
45540
45938
  init_version2();
45541
45939
  ALL_CHECKS = [
45542
45940
  "paths",
@@ -45548,7 +45946,8 @@ var init_audit = __esm({
45548
45946
  "contradictions",
45549
45947
  "frontmatter",
45550
45948
  "ci-coverage",
45551
- "ci-secrets"
45949
+ "ci-secrets",
45950
+ "content-secrets"
45552
45951
  ];
45553
45952
  ALL_MCP_CHECKS = [
45554
45953
  "mcp-schema",
@@ -46219,11 +46618,22 @@ var init_fixer = __esm({
46219
46618
 
46220
46619
  // src/mcp/server.ts
46221
46620
  var server_exports = {};
46621
+ __export(server_exports, {
46622
+ server: () => server,
46623
+ startServer: () => startServer
46624
+ });
46222
46625
  import * as path10 from "node:path";
46626
+ function describeDisallowed(rawPath) {
46627
+ const m = rawPath.match(PATH_DISALLOWED);
46628
+ if (!m) return "unknown";
46629
+ return JSON.stringify(m[0]);
46630
+ }
46223
46631
  function validateProjectPath(rawPath) {
46224
46632
  if (!rawPath) return process.cwd();
46225
46633
  if (PATH_DISALLOWED.test(rawPath)) {
46226
- throw new Error("projectPath contains disallowed characters");
46634
+ throw new Error(
46635
+ `projectPath contains disallowed character ${describeDisallowed(rawPath)} (control chars and shell metacharacters are rejected)`
46636
+ );
46227
46637
  }
46228
46638
  const resolved = path10.resolve(rawPath);
46229
46639
  if (!isDirectory(resolved)) {
@@ -46233,12 +46643,28 @@ function validateProjectPath(rawPath) {
46233
46643
  }
46234
46644
  function validateFilePathInput(rawPath) {
46235
46645
  if (PATH_DISALLOWED.test(rawPath)) {
46236
- throw new Error("path contains disallowed characters");
46646
+ throw new Error(
46647
+ `path contains disallowed character ${describeDisallowed(rawPath)} (control chars and shell metacharacters are rejected)`
46648
+ );
46649
+ }
46650
+ }
46651
+ function resolveWithinRoot(filePath, root) {
46652
+ const resolvedRoot = path10.resolve(root);
46653
+ const resolved = path10.resolve(resolvedRoot, filePath);
46654
+ const rel = path10.relative(resolvedRoot, resolved);
46655
+ if (rel.startsWith("..") || path10.isAbsolute(rel)) {
46656
+ throw new Error("path escapes the project root");
46237
46657
  }
46658
+ return resolved;
46238
46659
  }
46239
- var contextCheckEnum, mcpCheckEnum, mcphCheckEnum, sessionCheckEnum, PATH_DISALLOWED, server, transport;
46660
+ async function startServer() {
46661
+ keepEncoderAlive(true);
46662
+ const transport = new StdioServerTransport();
46663
+ await server.connect(transport);
46664
+ }
46665
+ var contextCheckEnum, mcpCheckEnum, mcphCheckEnum, sessionCheckEnum, PATH_DISALLOWED, server;
46240
46666
  var init_server3 = __esm({
46241
- async "src/mcp/server.ts"() {
46667
+ "src/mcp/server.ts"() {
46242
46668
  "use strict";
46243
46669
  init_mcp();
46244
46670
  init_stdio2();
@@ -46312,7 +46738,7 @@ var init_server3 = __esm({
46312
46738
  try {
46313
46739
  validateFilePathInput(filePath);
46314
46740
  const root = validateProjectPath(projectPath);
46315
- const resolved = path10.resolve(root, filePath);
46741
+ const resolved = resolveWithinRoot(filePath, root);
46316
46742
  const result = {
46317
46743
  path: filePath,
46318
46744
  exists: fileExists(resolved) || isDirectory(resolved)
@@ -46555,9 +46981,6 @@ var init_server3 = __esm({
46555
46981
  }
46556
46982
  }
46557
46983
  );
46558
- keepEncoderAlive(true);
46559
- transport = new StdioServerTransport();
46560
- await server.connect(transport);
46561
46984
  }
46562
46985
  });
46563
46986
 
@@ -53035,6 +53458,7 @@ var init_ora = __esm({
53035
53458
 
53036
53459
  // src/core/reporter.ts
53037
53460
  function classifyFile(f) {
53461
+ if (f.path === "(project)") return "context";
53038
53462
  if (f.path === "(mcp)") return "mcp";
53039
53463
  if (f.path.includes("session audit")) return "session";
53040
53464
  for (const issue2 of f.issues) {
@@ -53298,6 +53722,11 @@ function buildRuleDescriptors() {
53298
53722
  shortDescription: { text: "CI secret not documented in context files" },
53299
53723
  helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
53300
53724
  },
53725
+ {
53726
+ id: "ctxlint/content-secrets",
53727
+ shortDescription: { text: "Inline-pasted secret detected in a context file" },
53728
+ helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
53729
+ },
53301
53730
  {
53302
53731
  id: "ctxlint/mcp-schema",
53303
53732
  shortDescription: { text: "MCP config structural validation error" },
@@ -53494,7 +53923,8 @@ function suggestKey(unknown2) {
53494
53923
  best = known;
53495
53924
  }
53496
53925
  }
53497
- if (best && bestDist <= Math.max(2, Math.floor(unknown2.length / 3))) {
53926
+ const threshold = Math.min(4, Math.max(2, Math.floor(unknown2.length / 3)));
53927
+ if (best && bestDist <= threshold) {
53498
53928
  return best;
53499
53929
  }
53500
53930
  return null;
@@ -53532,7 +53962,7 @@ function loadConfig(projectRoot) {
53532
53962
  const filePath = path11.join(projectRoot, filename);
53533
53963
  let content;
53534
53964
  try {
53535
- content = fs8.readFileSync(filePath, "utf-8");
53965
+ content = stripBom(fs8.readFileSync(filePath, "utf-8"));
53536
53966
  } catch {
53537
53967
  continue;
53538
53968
  }
@@ -53543,7 +53973,7 @@ function loadConfig(projectRoot) {
53543
53973
  function loadConfigFromExplicitPath(configPath) {
53544
53974
  let content;
53545
53975
  try {
53546
- content = fs8.readFileSync(configPath, "utf-8");
53976
+ content = stripBom(fs8.readFileSync(configPath, "utf-8"));
53547
53977
  } catch (err) {
53548
53978
  const detail = err instanceof Error ? err.message : String(err);
53549
53979
  throw new Error(`could not load config from ${configPath}: ${detail}`, { cause: err });
@@ -53555,6 +53985,7 @@ var init_config2 = __esm({
53555
53985
  "src/core/config.ts"() {
53556
53986
  "use strict";
53557
53987
  import_fast_levenshtein2 = __toESM(require_levenshtein(), 1);
53988
+ init_fs();
53558
53989
  levenshtein2 = import_fast_levenshtein2.default.get;
53559
53990
  KNOWN_CONFIG_KEYS = [
53560
53991
  "checks",
@@ -53621,7 +54052,8 @@ async function runCli() {
53621
54052
  mcphOnly: options.mcphOnly,
53622
54053
  mcphStrictEnvToken: options.mcphStrictEnvToken,
53623
54054
  session: options.session,
53624
- sessionOnly: options.sessionOnly
54055
+ sessionOnly: options.sessionOnly,
54056
+ tokenThresholds: config2?.tokenThresholds
53625
54057
  });
53626
54058
  spinner?.stop();
53627
54059
  if (result.files.length === 0) {
@@ -53714,7 +54146,6 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53714
54146
  resetGit();
53715
54147
  resetPathsCache();
53716
54148
  resetPackageJsonCache();
53717
- resetTokenThresholds();
53718
54149
  }
53719
54150
  if (opts.watch) {
53720
54151
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
@@ -53776,11 +54207,6 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53776
54207
  console.error("Error reloading config:", err instanceof Error ? err.message : err);
53777
54208
  }
53778
54209
  try {
53779
- if (liveConfig?.tokenThresholds) {
53780
- setTokenThresholds(liveConfig.tokenThresholds);
53781
- } else {
53782
- resetTokenThresholds();
53783
- }
53784
54210
  const result = await runAudit(resolvedPath, liveActiveChecks, {
53785
54211
  depth: liveOptions.depth,
53786
54212
  extraPatterns: liveConfig?.contextFiles,
@@ -53792,7 +54218,8 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53792
54218
  mcphOnly: liveOptions.mcphOnly,
53793
54219
  mcphStrictEnvToken: liveOptions.mcphStrictEnvToken,
53794
54220
  session: liveOptions.session,
53795
- sessionOnly: liveOptions.sessionOnly
54221
+ sessionOnly: liveOptions.sessionOnly,
54222
+ tokenThresholds: liveConfig?.tokenThresholds
53796
54223
  });
53797
54224
  if (result.files.length === 0) {
53798
54225
  console.log("\nNo context files found.\n");
@@ -53812,7 +54239,6 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53812
54239
  resetGit();
53813
54240
  resetPathsCache();
53814
54241
  resetPackageJsonCache();
53815
- resetTokenThresholds();
53816
54242
  }
53817
54243
  console.log(chalk2.dim("\nWatching for changes... (Ctrl+C to stop)\n"));
53818
54244
  }, 300);
@@ -53956,9 +54382,6 @@ function resolveSession(resolvedPath, opts) {
53956
54382
  session: effectiveSession,
53957
54383
  sessionOnly
53958
54384
  };
53959
- if (config2?.tokenThresholds) {
53960
- setTokenThresholds(config2.tokenThresholds);
53961
- }
53962
54385
  const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
53963
54386
  return { config: config2, options, activeChecks };
53964
54387
  }
@@ -53978,7 +54401,6 @@ var init_cli = __esm({
53978
54401
  init_esm3();
53979
54402
  init_ora();
53980
54403
  init_paths();
53981
- init_tokens2();
53982
54404
  init_reporter();
53983
54405
  init_fixer();
53984
54406
  init_tokens();
@@ -53999,7 +54421,8 @@ var init_cli = __esm({
53999
54421
  // src/index.ts
54000
54422
  var args = process.argv.slice(2);
54001
54423
  if (args[0] === "serve" || args.includes("--mcp-server")) {
54002
- await init_server3().then(() => server_exports);
54424
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server3(), server_exports));
54425
+ await startServer2();
54003
54426
  } else {
54004
54427
  const { runCli: runCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
54005
54428
  await runCli2();