@yawlabs/ctxlint 0.9.20 → 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 (2) hide show
  1. package/dist/index.js +351 -109
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -34452,6 +34452,8 @@ async function scanGlobalMcpConfigs() {
34452
34452
  } catch {
34453
34453
  continue;
34454
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;
34455
34457
  const symlink = isSymlink(normalized);
34456
34458
  const target = symlink ? readSymlinkTarget(normalized) : void 0;
34457
34459
  found.push({
@@ -34464,6 +34466,14 @@ async function scanGlobalMcpConfigs() {
34464
34466
  }
34465
34467
  return found.sort((a, b2) => a.relativePath.localeCompare(b2.relativePath));
34466
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
+ }
34467
34477
  var CONTEXT_FILE_PATTERNS, IGNORED_DIRS2, MCP_CONFIG_PATTERNS, MCPH_CONFIG_PATTERNS;
34468
34478
  var init_scanner = __esm({
34469
34479
  "src/core/scanner.ts"() {
@@ -40699,12 +40709,13 @@ async function parseMcpConfig(file2, projectRoot, scope) {
40699
40709
  return result;
40700
40710
  }
40701
40711
  const lines = content.split("\n");
40712
+ const rootKeyLine = findRootKeyLine(lines, rootKey);
40702
40713
  for (const [name, value] of Object.entries(serversObj)) {
40703
40714
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
40704
40715
  continue;
40705
40716
  }
40706
40717
  const raw = value;
40707
- const line = findServerLine(lines, name);
40718
+ const line = findServerLine(lines, name, rootKeyLine);
40708
40719
  const transport = inferTransport(raw);
40709
40720
  const entry = {
40710
40721
  name,
@@ -40755,15 +40766,24 @@ function inferTransport(raw) {
40755
40766
  if ("url" in raw) return "http";
40756
40767
  return "unknown";
40757
40768
  }
40758
- function findServerLine(lines, serverName) {
40759
- const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40769
+ function findRootKeyLine(lines, rootKey) {
40770
+ const escaped = rootKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40760
40771
  const pattern = new RegExp(`"${escaped}"\\s*:`);
40761
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++) {
40762
40782
  if (pattern.test(lines[i2])) {
40763
40783
  return i2 + 1;
40764
40784
  }
40765
40785
  }
40766
- return 1;
40786
+ return rootKeyLine >= 0 ? rootKeyLine + 1 : 1;
40767
40787
  }
40768
40788
  function isStringRecord(value) {
40769
40789
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
@@ -41295,11 +41315,11 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
41295
41315
  onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
41296
41316
  ensurePropertyComplete(offset + length);
41297
41317
  },
41298
- onSeparator: (sep, offset, length) => {
41318
+ onSeparator: (sep2, offset, length) => {
41299
41319
  if (currentParent.type === "property") {
41300
- if (sep === ":") {
41320
+ if (sep2 === ":") {
41301
41321
  currentParent.colonOffset = offset;
41302
- } else if (sep === ",") {
41322
+ } else if (sep2 === ",") {
41303
41323
  ensurePropertyComplete(offset);
41304
41324
  }
41305
41325
  }
@@ -41807,7 +41827,6 @@ async function parseMcphConfig(file2, projectRoot, scopeOverride) {
41807
41827
  }
41808
41828
  function detectScope(relativePath) {
41809
41829
  const normalized = relativePath.replace(/\\/g, "/");
41810
- if (normalized.startsWith("~/")) return "global";
41811
41830
  if (normalized.endsWith(".mcph.local.json")) return "project-local";
41812
41831
  return "project";
41813
41832
  }
@@ -42424,26 +42443,21 @@ var init_staleness = __esm({
42424
42443
  });
42425
42444
 
42426
42445
  // src/core/checks/tokens.ts
42427
- function setTokenThresholds(overrides) {
42428
- const merged = { ...DEFAULT_THRESHOLDS, ...overrides };
42446
+ function resolveTokenThresholds(overrides) {
42447
+ if (!overrides) return DEFAULT_TOKEN_THRESHOLDS;
42448
+ const merged = { ...DEFAULT_TOKEN_THRESHOLDS, ...overrides };
42429
42449
  if (merged.info >= merged.warning || merged.warning >= merged.error) {
42430
42450
  console.error(
42431
42451
  `Warning: token thresholds should satisfy info < warning < error (got ${merged.info}, ${merged.warning}, ${merged.error}) \u2014 using defaults`
42432
42452
  );
42433
- return;
42453
+ return DEFAULT_TOKEN_THRESHOLDS;
42434
42454
  }
42435
- currentThresholds = merged;
42455
+ return merged;
42436
42456
  }
42437
- function resetTokenThresholds() {
42438
- currentThresholds = DEFAULT_THRESHOLDS;
42439
- }
42440
- function getTokenThresholds() {
42441
- return currentThresholds;
42442
- }
42443
- async function checkTokens(file2, _projectRoot) {
42457
+ async function checkTokens(file2, _projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42444
42458
  const issues = [];
42445
42459
  const tokens = file2.totalTokens;
42446
- if (tokens >= currentThresholds.error) {
42460
+ if (tokens >= thresholds.error) {
42447
42461
  issues.push({
42448
42462
  severity: "error",
42449
42463
  check: "tokens",
@@ -42452,7 +42466,7 @@ async function checkTokens(file2, _projectRoot) {
42452
42466
  message: `${tokens.toLocaleString()} tokens \u2014 consumes significant context window space`,
42453
42467
  suggestion: "Consider splitting into focused sections or removing redundant content."
42454
42468
  });
42455
- } else if (tokens >= currentThresholds.warning) {
42469
+ } else if (tokens >= thresholds.warning) {
42456
42470
  issues.push({
42457
42471
  severity: "warning",
42458
42472
  check: "tokens",
@@ -42461,7 +42475,7 @@ async function checkTokens(file2, _projectRoot) {
42461
42475
  message: `${tokens.toLocaleString()} tokens \u2014 large context file`,
42462
42476
  suggestion: "Consider trimming \u2014 research shows diminishing returns past ~300 lines."
42463
42477
  });
42464
- } else if (tokens >= currentThresholds.info) {
42478
+ } else if (tokens >= thresholds.info) {
42465
42479
  issues.push({
42466
42480
  severity: "info",
42467
42481
  check: "tokens",
@@ -42472,9 +42486,9 @@ async function checkTokens(file2, _projectRoot) {
42472
42486
  }
42473
42487
  return issues;
42474
42488
  }
42475
- function checkAggregateTokens(files) {
42489
+ function checkAggregateTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42476
42490
  const total = files.reduce((sum, f) => sum + f.tokens, 0);
42477
- if (total > currentThresholds.aggregate && files.length > 1) {
42491
+ if (total > thresholds.aggregate && files.length > 1) {
42478
42492
  return {
42479
42493
  severity: "warning",
42480
42494
  check: "tokens",
@@ -42486,11 +42500,11 @@ function checkAggregateTokens(files) {
42486
42500
  }
42487
42501
  return null;
42488
42502
  }
42489
- var DEFAULT_THRESHOLDS, currentThresholds;
42503
+ var DEFAULT_TOKEN_THRESHOLDS;
42490
42504
  var init_tokens2 = __esm({
42491
42505
  "src/core/checks/tokens.ts"() {
42492
42506
  "use strict";
42493
- DEFAULT_THRESHOLDS = {
42507
+ DEFAULT_TOKEN_THRESHOLDS = {
42494
42508
  info: 1e3,
42495
42509
  warning: 3e3,
42496
42510
  error: 8e3,
@@ -42498,7 +42512,6 @@ var init_tokens2 = __esm({
42498
42512
  tierBreakdown: 1e3,
42499
42513
  tierAggregate: 4e3
42500
42514
  };
42501
- currentThresholds = DEFAULT_THRESHOLDS;
42502
42515
  }
42503
42516
  });
42504
42517
 
@@ -42624,10 +42637,10 @@ function checkHardEnforcement(file2, settings) {
42624
42637
  }
42625
42638
  return issues;
42626
42639
  }
42627
- async function checkTierTokens(file2, projectRoot) {
42640
+ async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42628
42641
  if (!isAlwaysLoaded(file2)) return [];
42629
42642
  const issues = [];
42630
- const threshold = getTokenThresholds().tierBreakdown;
42643
+ const threshold = thresholds.tierBreakdown;
42631
42644
  if (file2.totalTokens >= threshold) {
42632
42645
  const sectionCosts = computeSectionCosts(file2);
42633
42646
  if (sectionCosts.length > 0) {
@@ -42650,11 +42663,11 @@ async function checkTierTokens(file2, projectRoot) {
42650
42663
  issues.push(...checkHardEnforcement(file2, settings));
42651
42664
  return issues;
42652
42665
  }
42653
- function checkAggregateTierTokens(files) {
42666
+ function checkAggregateTierTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42654
42667
  const alwaysLoaded = files.filter(isAlwaysLoaded);
42655
42668
  if (alwaysLoaded.length < 2) return null;
42656
42669
  const total = alwaysLoaded.reduce((sum, f) => sum + f.totalTokens, 0);
42657
- const threshold = getTokenThresholds().tierAggregate;
42670
+ const threshold = thresholds.tierAggregate;
42658
42671
  if (total < threshold) return null;
42659
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");
42660
42673
  return {
@@ -43457,7 +43470,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43457
43470
  issues.push({
43458
43471
  severity: "error",
43459
43472
  check: "mcp-schema",
43460
- ruleId: "invalid-json",
43473
+ ruleId: "mcp-schema/invalid-json",
43461
43474
  line: 1,
43462
43475
  message: `MCP config is not valid JSON: ${err}`
43463
43476
  });
@@ -43468,7 +43481,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43468
43481
  issues.push({
43469
43482
  severity: "error",
43470
43483
  check: "mcp-schema",
43471
- ruleId: "missing-root-key",
43484
+ ruleId: "mcp-schema/missing-root-key",
43472
43485
  line: 1,
43473
43486
  message: `MCP config has no "${config2.expectedRootKey}" key`
43474
43487
  });
@@ -43479,7 +43492,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43479
43492
  issues.push({
43480
43493
  severity: "error",
43481
43494
  check: "mcp-schema",
43482
- ruleId: "wrong-root-key",
43495
+ ruleId: "mcp-schema/wrong-root-key",
43483
43496
  line,
43484
43497
  message: `${config2.relativePath} must use "${config2.expectedRootKey}" as root key, not "${config2.actualRootKey}"`,
43485
43498
  fix: {
@@ -43494,7 +43507,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43494
43507
  issues.push({
43495
43508
  severity: "info",
43496
43509
  check: "mcp-schema",
43497
- ruleId: "empty-servers",
43510
+ ruleId: "mcp-schema/empty-servers",
43498
43511
  line: 1,
43499
43512
  message: "MCP config has no server entries"
43500
43513
  });
@@ -43505,7 +43518,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43505
43518
  issues.push({
43506
43519
  severity: "error",
43507
43520
  check: "mcp-schema",
43508
- ruleId: "no-name-field",
43521
+ ruleId: "mcp-schema/no-name-field",
43509
43522
  line: server2.line,
43510
43523
  message: "Server name cannot be empty"
43511
43524
  });
@@ -43517,7 +43530,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43517
43530
  issues.push({
43518
43531
  severity: "warning",
43519
43532
  check: "mcp-schema",
43520
- ruleId: "unknown-transport",
43533
+ ruleId: "mcp-schema/unknown-transport",
43521
43534
  line: server2.line,
43522
43535
  message: `Server "${server2.name}" has unknown transport type "${typeVal}"`
43523
43536
  });
@@ -43527,7 +43540,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43527
43540
  issues.push({
43528
43541
  severity: "warning",
43529
43542
  check: "mcp-schema",
43530
- ruleId: "ambiguous-transport",
43543
+ ruleId: "mcp-schema/ambiguous-transport",
43531
43544
  line: server2.line,
43532
43545
  message: `Server "${server2.name}" has both "command" and "url" \u2014 transport is ambiguous`
43533
43546
  });
@@ -43536,7 +43549,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43536
43549
  issues.push({
43537
43550
  severity: "error",
43538
43551
  check: "mcp-schema",
43539
- ruleId: "missing-command",
43552
+ ruleId: "mcp-schema/missing-command",
43540
43553
  line: server2.line,
43541
43554
  message: `Server "${server2.name}" has no "command" field`
43542
43555
  });
@@ -43545,7 +43558,7 @@ async function checkMcpSchema(config2, _projectRoot) {
43545
43558
  issues.push({
43546
43559
  severity: "error",
43547
43560
  check: "mcp-schema",
43548
- ruleId: "missing-url",
43561
+ ruleId: "mcp-schema/missing-url",
43549
43562
  line: server2.line,
43550
43563
  message: `Server "${server2.name}" has no "url" field`
43551
43564
  });
@@ -43604,7 +43617,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43604
43617
  issues.push({
43605
43618
  severity: "error",
43606
43619
  check: "mcp-security",
43607
- ruleId: "hardcoded-bearer",
43620
+ ruleId: "mcp-security/hardcoded-bearer",
43608
43621
  line: server2.line,
43609
43622
  message: `Server "${server2.name}" has a hardcoded Bearer token in a git-tracked file`,
43610
43623
  fix: {
@@ -43621,7 +43634,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43621
43634
  issues.push({
43622
43635
  severity: "error",
43623
43636
  check: "mcp-security",
43624
- ruleId: "hardcoded-api-key",
43637
+ ruleId: "mcp-security/hardcoded-api-key",
43625
43638
  line: server2.line,
43626
43639
  message: `Server "${server2.name}" has a hardcoded API key in a git-tracked file`
43627
43640
  });
@@ -43636,7 +43649,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43636
43649
  issues.push({
43637
43650
  severity: "error",
43638
43651
  check: "mcp-security",
43639
- ruleId: "hardcoded-api-key",
43652
+ ruleId: "mcp-security/hardcoded-api-key",
43640
43653
  line: server2.line,
43641
43654
  message: `Server "${server2.name}" has a hardcoded API key in a git-tracked file`,
43642
43655
  fix: {
@@ -43653,7 +43666,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43653
43666
  issues.push({
43654
43667
  severity: "error",
43655
43668
  check: "mcp-security",
43656
- ruleId: "secret-in-url",
43669
+ ruleId: "mcp-security/secret-in-url",
43657
43670
  line: server2.line,
43658
43671
  message: `Server "${server2.name}" has a secret in the URL query string`
43659
43672
  });
@@ -43666,7 +43679,7 @@ async function checkMcpSecurity(config2, _projectRoot) {
43666
43679
  issues.push({
43667
43680
  severity: "warning",
43668
43681
  check: "mcp-security",
43669
- ruleId: "http-no-tls",
43682
+ ruleId: "mcp-security/http-no-tls",
43670
43683
  line: server2.line,
43671
43684
  message: `Server "${server2.name}" uses HTTP without TLS`
43672
43685
  });
@@ -43738,7 +43751,7 @@ async function checkMcpCommands(config2, projectRoot) {
43738
43751
  issues.push({
43739
43752
  severity: "error",
43740
43753
  check: "mcp-commands",
43741
- ruleId: "windows-npx-no-wrapper",
43754
+ ruleId: "mcp-commands/windows-npx-no-wrapper",
43742
43755
  line: server2.line,
43743
43756
  message: `Server "${server2.name}": npx requires "cmd /c" wrapper on Windows`,
43744
43757
  suggestion: 'Change "command" to "cmd" and prepend "/c", "npx" to args \u2014 e.g. "args": ["/c", "npx", ...]'
@@ -43750,7 +43763,7 @@ async function checkMcpCommands(config2, projectRoot) {
43750
43763
  issues.push({
43751
43764
  severity: "warning",
43752
43765
  check: "mcp-commands",
43753
- ruleId: "command-not-found",
43766
+ ruleId: "mcp-commands/command-not-found",
43754
43767
  line: server2.line,
43755
43768
  message: `Server "${server2.name}": command "${server2.command}" not found`
43756
43769
  });
@@ -43765,7 +43778,7 @@ async function checkMcpCommands(config2, projectRoot) {
43765
43778
  issues.push({
43766
43779
  severity: "warning",
43767
43780
  check: "mcp-commands",
43768
- ruleId: "args-path-missing",
43781
+ ruleId: "mcp-commands/args-path-missing",
43769
43782
  line: server2.line,
43770
43783
  message: `Server "${server2.name}": arg "${arg}" looks like a file path but doesn't exist`
43771
43784
  });
@@ -43804,7 +43817,7 @@ async function checkMcpDeprecated(config2, _projectRoot) {
43804
43817
  issues.push({
43805
43818
  severity: "warning",
43806
43819
  check: "mcp-deprecated",
43807
- ruleId: "sse-transport",
43820
+ ruleId: "mcp-deprecated/sse-transport",
43808
43821
  line,
43809
43822
  message: `Server "${server2.name}" uses deprecated SSE transport \u2014 use "http" (Streamable HTTP) instead`,
43810
43823
  fix: {
@@ -43900,7 +43913,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43900
43913
  issues.push({
43901
43914
  severity: "error",
43902
43915
  check: "mcp-env",
43903
- ruleId: "wrong-syntax",
43916
+ ruleId: "mcp-env/wrong-syntax",
43904
43917
  line: server2.line,
43905
43918
  message: `Server "${server2.name}": Claude Code uses \${VAR}, not \${env:VAR}`,
43906
43919
  fix: buildSyntaxFix(config2, server2.line, value, "claude-code")
@@ -43912,7 +43925,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43912
43925
  issues.push({
43913
43926
  severity: "error",
43914
43927
  check: "mcp-env",
43915
- ruleId: "wrong-syntax",
43928
+ ruleId: "mcp-env/wrong-syntax",
43916
43929
  line: server2.line,
43917
43930
  message: `Server "${server2.name}": Cursor uses \${env:VAR}, not \${VAR}`,
43918
43931
  fix: buildSyntaxFix(config2, server2.line, value, "cursor")
@@ -43924,7 +43937,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43924
43937
  issues.push({
43925
43938
  severity: "error",
43926
43939
  check: "mcp-env",
43927
- ruleId: "wrong-syntax",
43940
+ ruleId: "mcp-env/wrong-syntax",
43928
43941
  line: server2.line,
43929
43942
  message: `Server "${server2.name}": Continue uses \${{ secrets.VAR }}, not \${VAR}`,
43930
43943
  fix: buildSyntaxFix(config2, server2.line, value, "continue")
@@ -43940,7 +43953,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43940
43953
  issues.push({
43941
43954
  severity: "info",
43942
43955
  check: "mcp-env",
43943
- ruleId: "unset-variable",
43956
+ ruleId: "mcp-env/unset-variable",
43944
43957
  line: server2.line,
43945
43958
  message: `Server "${server2.name}": environment variable "${ref.varName}" is not set`
43946
43959
  });
@@ -43952,7 +43965,7 @@ async function checkMcpEnv(config2, _projectRoot) {
43952
43965
  issues.push({
43953
43966
  severity: "info",
43954
43967
  check: "mcp-env",
43955
- ruleId: "empty-env-block",
43968
+ ruleId: "mcp-env/empty-env-block",
43956
43969
  line: server2.line,
43957
43970
  message: `Server "${server2.name}": empty "env" block can be removed`
43958
43971
  });
@@ -44008,7 +44021,7 @@ async function checkMcpUrls(config2, _projectRoot) {
44008
44021
  issues.push({
44009
44022
  severity: "error",
44010
44023
  check: "mcp-urls",
44011
- ruleId: "malformed-url",
44024
+ ruleId: "mcp-urls/malformed-url",
44012
44025
  line: server2.line,
44013
44026
  message: `Server "${server2.name}": invalid URL "${server2.url}"`
44014
44027
  });
@@ -44018,7 +44031,7 @@ async function checkMcpUrls(config2, _projectRoot) {
44018
44031
  issues.push({
44019
44032
  severity: "warning",
44020
44033
  check: "mcp-urls",
44021
- ruleId: "localhost-in-project-config",
44034
+ ruleId: "mcp-urls/localhost-in-project-config",
44022
44035
  line: server2.line,
44023
44036
  message: `Server "${server2.name}": localhost URL in project config won't work for teammates`
44024
44037
  });
@@ -44027,7 +44040,7 @@ async function checkMcpUrls(config2, _projectRoot) {
44027
44040
  issues.push({
44028
44041
  severity: "info",
44029
44042
  check: "mcp-urls",
44030
- ruleId: "missing-path",
44043
+ ruleId: "mcp-urls/missing-path",
44031
44044
  line: server2.line,
44032
44045
  message: `Server "${server2.name}": URL has no path \u2014 most MCP servers expect /mcp`
44033
44046
  });
@@ -44075,7 +44088,7 @@ async function checkMcpConsistency(configs) {
44075
44088
  issues.push({
44076
44089
  severity: "warning",
44077
44090
  check: "mcp-consistency",
44078
- ruleId: "same-server-different-config",
44091
+ ruleId: "mcp-consistency/same-server-different-config",
44079
44092
  line: a.line,
44080
44093
  message: `Server "${name}" is configured differently in ${a.config.relativePath} and ${b2.config.relativePath}`
44081
44094
  });
@@ -44101,7 +44114,7 @@ function checkMissingFromClient(configs) {
44101
44114
  issues.push({
44102
44115
  severity: "info",
44103
44116
  check: "mcp-consistency",
44104
- ruleId: "missing-from-client",
44117
+ ruleId: "mcp-consistency/missing-from-client",
44105
44118
  line: primaryServer.line,
44106
44119
  message: `Server "${primaryServer.name}" is in .mcp.json but missing from ${other.relativePath}`
44107
44120
  });
@@ -44178,7 +44191,7 @@ function checkSingleFileIssues(configs) {
44178
44191
  issues.push({
44179
44192
  severity: "warning",
44180
44193
  check: "mcp-consistency",
44181
- ruleId: "duplicate-server-name",
44194
+ ruleId: "mcp-consistency/duplicate-server-name",
44182
44195
  line: 1,
44183
44196
  message: `Duplicate server name "${name}" in ${config2.relativePath} \u2014 only the last definition is used`
44184
44197
  });
@@ -44202,7 +44215,7 @@ async function checkMcpRedundancy(configs) {
44202
44215
  issues.push({
44203
44216
  severity: "info",
44204
44217
  check: "mcp-redundancy",
44205
- ruleId: "disabled-server",
44218
+ ruleId: "mcp-redundancy/disabled-server",
44206
44219
  line: server2.line,
44207
44220
  message: `Server "${server2.name}" is disabled \u2014 consider removing it if no longer needed`
44208
44221
  });
@@ -44232,7 +44245,7 @@ async function checkMcpRedundancy(configs) {
44232
44245
  issues.push({
44233
44246
  severity: "info",
44234
44247
  check: "mcp-redundancy",
44235
- ruleId: "identical-across-scopes",
44248
+ ruleId: "mcp-redundancy/identical-across-scopes",
44236
44249
  line: projectServer.line,
44237
44250
  message: `Server "${projectServer.name}" is identically configured in both ${projectConfig.relativePath} and ${globalConfig2.relativePath}`
44238
44251
  });
@@ -44258,7 +44271,7 @@ async function checkMcphTokenSecurity(config2, _projectRoot, options = {}) {
44258
44271
  issues.push({
44259
44272
  severity: "error",
44260
44273
  check: "mcph-token-security",
44261
- ruleId: "mcph-config/invalid-token-format",
44274
+ ruleId: "mcph-token-security/invalid-token-format",
44262
44275
  line: tokenPos.line,
44263
44276
  message: `"token" does not match expected format ^mcp_pat_[A-Za-z0-9_-]+$`,
44264
44277
  suggestion: `Check that the token was copied in full. A valid mcp.hosting PAT looks like: mcp_pat_aBcDeFg123...
@@ -44269,7 +44282,7 @@ If the token was truncated or wrapped in quotes elsewhere, re-issue it from http
44269
44282
  issues.push({
44270
44283
  severity: "error",
44271
44284
  check: "mcph-token-security",
44272
- ruleId: "mcph-config/token-in-project-scope",
44285
+ ruleId: "mcph-token-security/token-in-project-scope",
44273
44286
  line: tokenPos.line,
44274
44287
  message: `"token" in a git-tracked project-scope .mcph.json \u2014 PAT will leak via git history`,
44275
44288
  suggestion: `Delete line ${tokenPos.line} (the "token" field) from ${config2.relativePath}.
@@ -44287,7 +44300,7 @@ If this token was already committed, ROTATE it now: https://mcp.hosting/settings
44287
44300
  issues.push({
44288
44301
  severity,
44289
44302
  check: "mcph-token-security",
44290
- ruleId: "mcph-config/prefer-env-token",
44303
+ ruleId: "mcph-token-security/prefer-env-token",
44291
44304
  line: tokenPos.line,
44292
44305
  message: `prefer MCPH_TOKEN env var over a file-stored token in ${config2.relativePath}`,
44293
44306
  suggestion: `Delete line ${tokenPos.line} (the "token" field) and export instead:
@@ -44328,7 +44341,7 @@ async function checkMcphApibase(config2, _projectRoot) {
44328
44341
  issues.push({
44329
44342
  severity: "error",
44330
44343
  check: "mcph-apibase",
44331
- ruleId: "mcph-config/invalid-apibase",
44344
+ ruleId: "mcph-apibase/invalid-apibase",
44332
44345
  line: pos.line,
44333
44346
  message: `"apiBase" is not a valid URL: ${value}`,
44334
44347
  suggestion: `Use an absolute http(s) URL, e.g. "https://mcp.hosting".`
@@ -44339,7 +44352,7 @@ async function checkMcphApibase(config2, _projectRoot) {
44339
44352
  issues.push({
44340
44353
  severity: "warning",
44341
44354
  check: "mcph-apibase",
44342
- ruleId: "mcph-config/insecure-apibase",
44355
+ ruleId: "mcph-apibase/insecure-apibase",
44343
44356
  line: pos.line,
44344
44357
  message: `"apiBase" uses plaintext HTTP to a public host (${parsed.hostname})`,
44345
44358
  suggestion: `Use https:// instead. Plaintext HTTP exposes your MCPH_TOKEN on the wire.
@@ -44375,7 +44388,7 @@ async function checkMcphSchemaConformance(config2, _projectRoot) {
44375
44388
  issues.push({
44376
44389
  severity: "warning",
44377
44390
  check: "mcph-schema-conformance",
44378
- ruleId: "mcph-config/unknown-field",
44391
+ ruleId: "mcph-schema-conformance/unknown-field",
44379
44392
  line: field.position.line,
44380
44393
  message: `unknown field "${field.name}" \u2014 not in the mcph config schema`,
44381
44394
  suggestion: `Known fields: $schema, version, token, apiBase, servers, blocked. Check for typos (e.g. "tokens" vs "token", "blockList" vs "blocked").`
@@ -44387,7 +44400,7 @@ async function checkMcphSchemaConformance(config2, _projectRoot) {
44387
44400
  issues.push({
44388
44401
  severity: "info",
44389
44402
  check: "mcph-schema-conformance",
44390
- ruleId: "mcph-config/stale-version",
44403
+ ruleId: "mcph-schema-conformance/stale-version",
44391
44404
  line: versionPos.line,
44392
44405
  message: `"version": ${version2} is older than the current schema version (${CURRENT_SCHEMA_VERSION})`,
44393
44406
  suggestion: `Update to "version": ${CURRENT_SCHEMA_VERSION}. Older versions continue to load but may miss newer fields.`
@@ -44415,7 +44428,7 @@ async function checkMcphLists(config2, _projectRoot) {
44415
44428
  issues.push({
44416
44429
  severity: "warning",
44417
44430
  check: "mcph-lists",
44418
- ruleId: "mcph-config/allowlist-denylist-conflict",
44431
+ ruleId: "mcph-lists/allowlist-denylist-conflict",
44419
44432
  line: entry.position.line,
44420
44433
  message: `server "${entry.value}" is in both "servers" (allow-list) and "blocked" (deny-list)`,
44421
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.`
@@ -44431,7 +44444,7 @@ async function checkMcphLists(config2, _projectRoot) {
44431
44444
  issues.push({
44432
44445
  severity: "info",
44433
44446
  check: "mcph-lists",
44434
- ruleId: "mcph-config/duplicate-entries",
44447
+ ruleId: "mcph-lists/duplicate-entries",
44435
44448
  line: entry.position.line,
44436
44449
  message: `"${entry.value}" appears multiple times in "${listName}" (first at line ${prevLine})`,
44437
44450
  suggestion: `Remove the duplicate entry at line ${entry.position.line}.`
@@ -44458,7 +44471,7 @@ async function checkMcphGitignore(config2, _projectRoot) {
44458
44471
  issues.push({
44459
44472
  severity: "error",
44460
44473
  check: "mcph-gitignore",
44461
- ruleId: "mcph-config/local-file-not-gitignored",
44474
+ ruleId: "mcph-gitignore/local-file-not-gitignored",
44462
44475
  line: 1,
44463
44476
  message: `${basename4} is not covered by .gitignore \u2014 machine-local overrides can leak via git`,
44464
44477
  suggestion: `Add "${basename4}" to .gitignore in your project root.`
@@ -44801,7 +44814,7 @@ async function checkMissingSecret(ctx) {
44801
44814
  issues.push({
44802
44815
  severity: "error",
44803
44816
  check: "session-missing-secret",
44804
- ruleId: "session/missing-secret",
44817
+ ruleId: "session-missing-secret/missing-secret",
44805
44818
  line: 0,
44806
44819
  message: `GitHub secret "${secretName}" is set on ${siblingMatches.length} sibling repos (${sibNames}) but not on this project`,
44807
44820
  suggestion: `Run: gh secret set ${secretName} --repo <owner>/<repo>`,
@@ -44879,7 +44892,7 @@ async function checkDivergedFile(ctx) {
44879
44892
  issues.push({
44880
44893
  severity: "warning",
44881
44894
  check: "session-diverged-file",
44882
- ruleId: "session/diverged-file",
44895
+ ruleId: "session-diverged-file/diverged-file",
44883
44896
  line: 0,
44884
44897
  message: `${fileName} has diverged from sibling repos: ${details}`,
44885
44898
  suggestion: `Compare with sibling versions to identify unintentional drift`,
@@ -44951,7 +44964,7 @@ async function checkMissingWorkflow(ctx) {
44951
44964
  issues.push({
44952
44965
  severity: "warning",
44953
44966
  check: "session-missing-workflow",
44954
- ruleId: "session/missing-workflow",
44967
+ ruleId: "session-missing-workflow/missing-workflow",
44955
44968
  line: 0,
44956
44969
  message: `GitHub Actions workflow "${workflow}" exists in ${siblings.length} sibling repos (${sibNames}) but not in this project`,
44957
44970
  suggestion: `Consider adding .github/workflows/${workflow} for consistency`,
@@ -44997,7 +45010,7 @@ async function checkStaleMemory(ctx) {
44997
45010
  issues.push({
44998
45011
  severity: "info",
44999
45012
  check: "session-stale-memory",
45000
- ruleId: "session/stale-memory",
45013
+ ruleId: "session-stale-memory/stale-memory",
45001
45014
  line: 0,
45002
45015
  message: `Memory "${name}" references ${brokenPaths.length} path(s) that no longer exist: ${brokenPaths.join(", ")}`,
45003
45016
  suggestion: `Update or remove the memory file: ${mem.filePath}`,
@@ -45040,7 +45053,7 @@ async function checkDuplicateMemory(ctx) {
45040
45053
  issues.push({
45041
45054
  severity: "info",
45042
45055
  check: "session-duplicate-memory",
45043
- ruleId: "session/duplicate-memory",
45056
+ ruleId: "session-duplicate-memory/duplicate-memory",
45044
45057
  line: 0,
45045
45058
  message: `Memory "${nameA}" (${projA}) and "${nameB}" (${projB}) have ${Math.round(overlap * 100)}% overlap`,
45046
45059
  suggestion: `Consider consolidating into a shared memory or removing the duplicate`,
@@ -45111,7 +45124,8 @@ function findCyclicPatterns(displays) {
45111
45124
  async function checkLoopDetection(ctx) {
45112
45125
  const issues = [];
45113
45126
  const currentNorm = normalizeProject(ctx.currentProject);
45114
- 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;
45115
45129
  if (entries.length < CONSECUTIVE_THRESHOLD) return issues;
45116
45130
  const displays = entries.map((e) => e.display);
45117
45131
  const repeats = findConsecutiveRepeats(displays);
@@ -45120,7 +45134,7 @@ async function checkLoopDetection(ctx) {
45120
45134
  issues.push({
45121
45135
  severity: "warning",
45122
45136
  check: "session-loop-detection",
45123
- ruleId: "session/consecutive-repeat",
45137
+ ruleId: "session-loop-detection/consecutive-repeat",
45124
45138
  line: 0,
45125
45139
  message: `Command run ${count} times consecutively: "${truncated}"`,
45126
45140
  suggestion: "An agent may be looping on this command. Check history.jsonl for context on what went wrong"
@@ -45132,7 +45146,7 @@ async function checkLoopDetection(ctx) {
45132
45146
  issues.push({
45133
45147
  severity: "warning",
45134
45148
  check: "session-loop-detection",
45135
- ruleId: "session/cyclic-pattern",
45149
+ ruleId: "session-loop-detection/cyclic-pattern",
45136
45150
  line: 0,
45137
45151
  message: `Cyclic pattern repeated ${reps} times: ${cycleStr}`,
45138
45152
  suggestion: "An agent may be stuck in a loop. Check if a context file is missing instructions for this workflow"
@@ -45140,13 +45154,14 @@ async function checkLoopDetection(ctx) {
45140
45154
  }
45141
45155
  return issues;
45142
45156
  }
45143
- var CONSECUTIVE_THRESHOLD, CYCLE_REPEAT_THRESHOLD, MAX_CYCLE_LENGTH;
45157
+ var CONSECUTIVE_THRESHOLD, CYCLE_REPEAT_THRESHOLD, MAX_CYCLE_LENGTH, MAX_HISTORY_ENTRIES;
45144
45158
  var init_loop_detection = __esm({
45145
45159
  "src/core/checks/session/loop-detection.ts"() {
45146
45160
  "use strict";
45147
45161
  CONSECUTIVE_THRESHOLD = 3;
45148
45162
  CYCLE_REPEAT_THRESHOLD = 2;
45149
45163
  MAX_CYCLE_LENGTH = 3;
45164
+ MAX_HISTORY_ENTRIES = 5e3;
45150
45165
  }
45151
45166
  });
45152
45167
 
@@ -45174,7 +45189,7 @@ async function checkMemoryIndexOverflow(ctx) {
45174
45189
  issues.push({
45175
45190
  severity: "warning",
45176
45191
  check: "session-memory-index-overflow",
45177
- ruleId: "session/memory-index-overflow",
45192
+ ruleId: "session-memory-index-overflow/memory-index-overflow",
45178
45193
  line: MAX_LINES + 1,
45179
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.`,
45180
45195
  detail: `File: ${memoryFile}`,
@@ -45186,7 +45201,7 @@ async function checkMemoryIndexOverflow(ctx) {
45186
45201
  issues.push({
45187
45202
  severity: "warning",
45188
45203
  check: "session-memory-index-overflow",
45189
- ruleId: "session/memory-index-overflow",
45204
+ ruleId: "session-memory-index-overflow/memory-index-overflow",
45190
45205
  line: 0,
45191
45206
  message: `MEMORY.md is ${byteSize.toLocaleString()} bytes \u2014 only the first ${MAX_BYTES.toLocaleString()} bytes are loaded. ~${excess.toLocaleString()} bytes are effectively invisible.`,
45192
45207
  detail: `File: ${memoryFile}`,
@@ -45253,7 +45268,7 @@ async function checkCiCoverage(files, projectRoot) {
45253
45268
  {
45254
45269
  severity: "info",
45255
45270
  check: "ci-coverage",
45256
- ruleId: "ci/no-release-docs",
45271
+ ruleId: "ci-coverage/no-release-docs",
45257
45272
  line: 0,
45258
45273
  message: `Release workflow${releaseWorkflows.length > 1 ? "s" : ""} found (${releaseWorkflows.join(", ")}) but no context file documents the release process`,
45259
45274
  suggestion: `Document how releases work (e.g. "push a v* tag to trigger CI") in a context file so agents don't guess`
@@ -45328,7 +45343,7 @@ async function checkCiSecrets(files, projectRoot) {
45328
45343
  issues.push({
45329
45344
  severity: "info",
45330
45345
  check: "ci-secrets",
45331
- ruleId: "ci/undocumented-secret",
45346
+ ruleId: "ci-secrets/undocumented-secret",
45332
45347
  line: 0,
45333
45348
  message: `CI secret "${name}" is used in ${workflows.join(", ")} but not mentioned in any context file`,
45334
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`
@@ -45356,15 +45371,239 @@ var init_ci_secrets = __esm({
45356
45371
  }
45357
45372
  });
45358
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
+
45359
45598
  // src/version.ts
45360
- import { readFileSync as readFileSync4 } from "node:fs";
45599
+ import { readFileSync as readFileSync5 } from "node:fs";
45361
45600
  import { resolve as resolve8, dirname as dirname3 } from "node:path";
45362
45601
  import { fileURLToPath } from "node:url";
45363
45602
  function loadVersion() {
45364
- if (true) return "0.9.20";
45603
+ if (true) return "0.10.0";
45365
45604
  const __dir = dirname3(fileURLToPath(import.meta.url));
45366
45605
  const pkgPath = resolve8(__dir, "../package.json");
45367
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
45606
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
45368
45607
  return pkg.version;
45369
45608
  }
45370
45609
  var VERSION;
@@ -45392,6 +45631,7 @@ function deriveChecksToRun(activeChecks, prefix, enabled, allChecks) {
45392
45631
  }
45393
45632
  async function runAudit(projectRoot, activeChecks, options = {}) {
45394
45633
  const fileResults = [];
45634
+ const thresholds = resolveTokenThresholds(options.tokenThresholds);
45395
45635
  const shouldRunContextChecks = !options.mcpOnly && !options.mcphOnly && !options.sessionOnly;
45396
45636
  const shouldRunMcpChecks = options.mcp || options.mcpGlobal || options.mcpOnly || hasMcpChecks(activeChecks);
45397
45637
  const shouldRunMcphChecks = options.mcph || options.mcphGlobal || options.mcphOnly || hasMcphChecks(activeChecks);
@@ -45407,13 +45647,16 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45407
45647
  if (activeChecks.includes("paths")) checkPromises.push(checkPaths(file2, projectRoot));
45408
45648
  if (activeChecks.includes("commands")) checkPromises.push(checkCommands(file2, projectRoot));
45409
45649
  if (activeChecks.includes("staleness")) checkPromises.push(checkStaleness(file2, projectRoot));
45410
- if (activeChecks.includes("tokens")) checkPromises.push(checkTokens(file2, projectRoot));
45650
+ if (activeChecks.includes("tokens"))
45651
+ checkPromises.push(checkTokens(file2, projectRoot, thresholds));
45411
45652
  if (activeChecks.includes("tier-tokens"))
45412
- checkPromises.push(checkTierTokens(file2, projectRoot));
45653
+ checkPromises.push(checkTierTokens(file2, projectRoot, thresholds));
45413
45654
  if (activeChecks.includes("redundancy"))
45414
45655
  checkPromises.push(checkRedundancy(file2, projectRoot));
45415
45656
  if (activeChecks.includes("frontmatter"))
45416
45657
  checkPromises.push(checkFrontmatter(file2, projectRoot));
45658
+ if (activeChecks.includes("content-secrets"))
45659
+ checkPromises.push(checkContentSecrets(file2, projectRoot));
45417
45660
  const results = await Promise.all(checkPromises);
45418
45661
  const issues = results.flat();
45419
45662
  fileResults.push({
@@ -45428,12 +45671,13 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45428
45671
  const crossFileIssues = [];
45429
45672
  if (activeChecks.includes("tokens")) {
45430
45673
  const aggIssue = checkAggregateTokens(
45431
- fileResults.map((f) => ({ path: f.path, tokens: f.tokens }))
45674
+ fileResults.map((f) => ({ path: f.path, tokens: f.tokens })),
45675
+ thresholds
45432
45676
  );
45433
45677
  if (aggIssue) crossFileIssues.push(aggIssue);
45434
45678
  }
45435
45679
  if (activeChecks.includes("tier-tokens")) {
45436
- const tierAgg = checkAggregateTierTokens(parsed);
45680
+ const tierAgg = checkAggregateTierTokens(parsed, thresholds);
45437
45681
  if (tierAgg) crossFileIssues.push(tierAgg);
45438
45682
  }
45439
45683
  if (activeChecks.includes("redundancy")) {
@@ -45569,7 +45813,7 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
45569
45813
  issues.push({
45570
45814
  severity: "error",
45571
45815
  check: "mcph-schema-conformance",
45572
- ruleId: "mcph-config/parse-error",
45816
+ ruleId: "mcph-schema-conformance/parse-error",
45573
45817
  line: 1,
45574
45818
  message: err
45575
45819
  });
@@ -45690,6 +45934,7 @@ var init_audit = __esm({
45690
45934
  init_memory_index_overflow();
45691
45935
  init_ci_coverage();
45692
45936
  init_ci_secrets();
45937
+ init_content_secrets();
45693
45938
  init_version2();
45694
45939
  ALL_CHECKS = [
45695
45940
  "paths",
@@ -45701,7 +45946,8 @@ var init_audit = __esm({
45701
45946
  "contradictions",
45702
45947
  "frontmatter",
45703
45948
  "ci-coverage",
45704
- "ci-secrets"
45949
+ "ci-secrets",
45950
+ "content-secrets"
45705
45951
  ];
45706
45952
  ALL_MCP_CHECKS = [
45707
45953
  "mcp-schema",
@@ -53214,7 +53460,6 @@ var init_ora = __esm({
53214
53460
  function classifyFile(f) {
53215
53461
  if (f.path === "(project)") return "context";
53216
53462
  if (f.path === "(mcp)") return "mcp";
53217
- if (f.path === "(mcph)") return "mcph";
53218
53463
  if (f.path.includes("session audit")) return "session";
53219
53464
  for (const issue2 of f.issues) {
53220
53465
  if (issue2.check.startsWith("session-")) return "session";
@@ -53477,6 +53722,11 @@ function buildRuleDescriptors() {
53477
53722
  shortDescription: { text: "CI secret not documented in context files" },
53478
53723
  helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
53479
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
+ },
53480
53730
  {
53481
53731
  id: "ctxlint/mcp-schema",
53482
53732
  shortDescription: { text: "MCP config structural validation error" },
@@ -53673,7 +53923,8 @@ function suggestKey(unknown2) {
53673
53923
  best = known;
53674
53924
  }
53675
53925
  }
53676
- 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) {
53677
53928
  return best;
53678
53929
  }
53679
53930
  return null;
@@ -53801,7 +54052,8 @@ async function runCli() {
53801
54052
  mcphOnly: options.mcphOnly,
53802
54053
  mcphStrictEnvToken: options.mcphStrictEnvToken,
53803
54054
  session: options.session,
53804
- sessionOnly: options.sessionOnly
54055
+ sessionOnly: options.sessionOnly,
54056
+ tokenThresholds: config2?.tokenThresholds
53805
54057
  });
53806
54058
  spinner?.stop();
53807
54059
  if (result.files.length === 0) {
@@ -53894,7 +54146,6 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53894
54146
  resetGit();
53895
54147
  resetPathsCache();
53896
54148
  resetPackageJsonCache();
53897
- resetTokenThresholds();
53898
54149
  }
53899
54150
  if (opts.watch) {
53900
54151
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
@@ -53956,11 +54207,6 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53956
54207
  console.error("Error reloading config:", err instanceof Error ? err.message : err);
53957
54208
  }
53958
54209
  try {
53959
- if (liveConfig?.tokenThresholds) {
53960
- setTokenThresholds(liveConfig.tokenThresholds);
53961
- } else {
53962
- resetTokenThresholds();
53963
- }
53964
54210
  const result = await runAudit(resolvedPath, liveActiveChecks, {
53965
54211
  depth: liveOptions.depth,
53966
54212
  extraPatterns: liveConfig?.contextFiles,
@@ -53972,7 +54218,8 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53972
54218
  mcphOnly: liveOptions.mcphOnly,
53973
54219
  mcphStrictEnvToken: liveOptions.mcphStrictEnvToken,
53974
54220
  session: liveOptions.session,
53975
- sessionOnly: liveOptions.sessionOnly
54221
+ sessionOnly: liveOptions.sessionOnly,
54222
+ tokenThresholds: liveConfig?.tokenThresholds
53976
54223
  });
53977
54224
  if (result.files.length === 0) {
53978
54225
  console.log("\nNo context files found.\n");
@@ -53992,7 +54239,6 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
53992
54239
  resetGit();
53993
54240
  resetPathsCache();
53994
54241
  resetPackageJsonCache();
53995
- resetTokenThresholds();
53996
54242
  }
53997
54243
  console.log(chalk2.dim("\nWatching for changes... (Ctrl+C to stop)\n"));
53998
54244
  }, 300);
@@ -54136,9 +54382,6 @@ function resolveSession(resolvedPath, opts) {
54136
54382
  session: effectiveSession,
54137
54383
  sessionOnly
54138
54384
  };
54139
- if (config2?.tokenThresholds) {
54140
- setTokenThresholds(config2.tokenThresholds);
54141
- }
54142
54385
  const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
54143
54386
  return { config: config2, options, activeChecks };
54144
54387
  }
@@ -54158,7 +54401,6 @@ var init_cli = __esm({
54158
54401
  init_esm3();
54159
54402
  init_ora();
54160
54403
  init_paths();
54161
- init_tokens2();
54162
54404
  init_reporter();
54163
54405
  init_fixer();
54164
54406
  init_tokens();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/ctxlint",
3
- "version": "0.9.20",
3
+ "version": "0.10.0",
4
4
  "description": "Lint your AI agent context files, MCP server configs, and session data against your actual codebase",
5
5
  "bin": {
6
6
  "ctxlint": "dist/index.js"