@yawlabs/ctxlint 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pre-commit-hooks.yaml +1 -1
- package/README.md +568 -568
- package/dist/index.js +127 -60
- package/mcph-config-lint-rules.json +11 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42489,7 +42489,7 @@ async function checkPaths(file2, projectRoot) {
|
|
|
42489
42489
|
const resolvedPath = path4.resolve(baseDir, ref.value);
|
|
42490
42490
|
const normalizedRef = ref.value.replace(/\\/g, "/");
|
|
42491
42491
|
if (normalizedRef.includes("*")) {
|
|
42492
|
-
const matches = await Ze(normalizedRef, { cwd: baseDir, nodir: false });
|
|
42492
|
+
const matches = path4.isAbsolute(normalizedRef) ? await Ze(normalizedRef, { absolute: true, nodir: false }) : await Ze(normalizedRef, { cwd: baseDir, nodir: false });
|
|
42493
42493
|
if (matches.length === 0) {
|
|
42494
42494
|
issues.push({
|
|
42495
42495
|
severity: "error",
|
|
@@ -42871,7 +42871,7 @@ async function checkTokens(file2, _projectRoot, thresholds = DEFAULT_TOKEN_THRES
|
|
|
42871
42871
|
}
|
|
42872
42872
|
function checkAggregateTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
|
|
42873
42873
|
const total = files.reduce((sum, f) => sum + f.tokens, 0);
|
|
42874
|
-
if (total
|
|
42874
|
+
if (total >= thresholds.aggregate && files.length > 1) {
|
|
42875
42875
|
return {
|
|
42876
42876
|
severity: "warning",
|
|
42877
42877
|
check: "tokens",
|
|
@@ -42950,14 +42950,20 @@ function computeSectionCosts(file2) {
|
|
|
42950
42950
|
return { title: s.title, line: s.startLine, tokens: countTokens(body) };
|
|
42951
42951
|
}).sort((a, b2) => b2.tokens - a.tokens);
|
|
42952
42952
|
}
|
|
42953
|
-
function loadSettingsSources(projectRoot) {
|
|
42954
|
-
if (settingsCache?.root === projectRoot
|
|
42953
|
+
function loadSettingsSources(projectRoot, includeGlobal) {
|
|
42954
|
+
if (settingsCache?.root === projectRoot && settingsCache.includeGlobal === includeGlobal) {
|
|
42955
|
+
return settingsCache.data;
|
|
42956
|
+
}
|
|
42955
42957
|
const sources = [];
|
|
42956
42958
|
const candidates = [
|
|
42957
42959
|
path7.join(projectRoot, ".claude", "settings.json"),
|
|
42958
|
-
path7.join(projectRoot, ".claude", "settings.local.json")
|
|
42959
|
-
path7.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
|
|
42960
|
+
path7.join(projectRoot, ".claude", "settings.local.json")
|
|
42960
42961
|
];
|
|
42962
|
+
if (includeGlobal) {
|
|
42963
|
+
candidates.push(
|
|
42964
|
+
path7.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
|
|
42965
|
+
);
|
|
42966
|
+
}
|
|
42961
42967
|
for (const p2 of candidates) {
|
|
42962
42968
|
let content;
|
|
42963
42969
|
try {
|
|
@@ -42971,7 +42977,7 @@ function loadSettingsSources(projectRoot) {
|
|
|
42971
42977
|
console.warn(`ctxlint: could not parse ${p2}: ${err.message}`);
|
|
42972
42978
|
}
|
|
42973
42979
|
}
|
|
42974
|
-
settingsCache = { root: projectRoot, data: sources };
|
|
42980
|
+
settingsCache = { root: projectRoot, includeGlobal, data: sources };
|
|
42975
42981
|
return sources;
|
|
42976
42982
|
}
|
|
42977
42983
|
function canonicalizeCommand(backticked) {
|
|
@@ -43023,7 +43029,7 @@ function checkHardEnforcement(file2, settings) {
|
|
|
43023
43029
|
}
|
|
43024
43030
|
return issues;
|
|
43025
43031
|
}
|
|
43026
|
-
async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
|
|
43032
|
+
async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS, includeGlobal = false) {
|
|
43027
43033
|
if (!isAlwaysLoaded(file2)) return [];
|
|
43028
43034
|
const issues = [];
|
|
43029
43035
|
const threshold = thresholds.tierBreakdown;
|
|
@@ -43045,7 +43051,7 @@ async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_TH
|
|
|
43045
43051
|
});
|
|
43046
43052
|
}
|
|
43047
43053
|
}
|
|
43048
|
-
const settings = loadSettingsSources(projectRoot);
|
|
43054
|
+
const settings = loadSettingsSources(projectRoot, includeGlobal);
|
|
43049
43055
|
issues.push(...checkHardEnforcement(file2, settings));
|
|
43050
43056
|
return issues;
|
|
43051
43057
|
}
|
|
@@ -44067,7 +44073,9 @@ async function checkMcpSecurity(config2, _projectRoot) {
|
|
|
44067
44073
|
try {
|
|
44068
44074
|
if (!isEnvVarRef(server2.url)) {
|
|
44069
44075
|
const parsed = new URL(server2.url);
|
|
44070
|
-
|
|
44076
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
44077
|
+
const isLoopback = host === "localhost" || host === "::1" || host.startsWith("127.");
|
|
44078
|
+
if (parsed.protocol === "http:" && !isLoopback) {
|
|
44071
44079
|
issues.push({
|
|
44072
44080
|
severity: "warning",
|
|
44073
44081
|
check: "mcp-security",
|
|
@@ -44523,6 +44531,7 @@ function checkMissingFromClient(configs) {
|
|
|
44523
44531
|
function collectServerNameKeys(content, rootKey) {
|
|
44524
44532
|
const names = [];
|
|
44525
44533
|
let i2 = 0;
|
|
44534
|
+
let keyStart = -1;
|
|
44526
44535
|
let depth = 0;
|
|
44527
44536
|
let inString = false;
|
|
44528
44537
|
let escape2 = false;
|
|
@@ -44548,6 +44557,7 @@ function collectServerNameKeys(content, rootKey) {
|
|
|
44548
44557
|
inString = true;
|
|
44549
44558
|
collectingKey = true;
|
|
44550
44559
|
pendingKey = "";
|
|
44560
|
+
keyStart = i2;
|
|
44551
44561
|
} else {
|
|
44552
44562
|
inString = false;
|
|
44553
44563
|
let j3 = i2 + 1;
|
|
@@ -44556,7 +44566,11 @@ function collectServerNameKeys(content, rootKey) {
|
|
|
44556
44566
|
if (pendingKey === rootKey && rootKeyDepth === -1) {
|
|
44557
44567
|
rootKeyDepth = depth;
|
|
44558
44568
|
} else if (rootKeyDepth !== -1 && depth === rootKeyDepth + 1) {
|
|
44559
|
-
|
|
44569
|
+
let line = 1;
|
|
44570
|
+
for (let k3 = 0; k3 < keyStart; k3++) {
|
|
44571
|
+
if (content[k3] === "\n") line++;
|
|
44572
|
+
}
|
|
44573
|
+
names.push({ name: pendingKey, line });
|
|
44560
44574
|
}
|
|
44561
44575
|
}
|
|
44562
44576
|
collectingKey = false;
|
|
@@ -44582,14 +44596,19 @@ function checkSingleFileIssues(configs) {
|
|
|
44582
44596
|
if (!config2.actualRootKey) continue;
|
|
44583
44597
|
const serverKeys = collectServerNameKeys(config2.content, config2.actualRootKey);
|
|
44584
44598
|
const counts = /* @__PURE__ */ new Map();
|
|
44585
|
-
|
|
44599
|
+
const secondLines = /* @__PURE__ */ new Map();
|
|
44600
|
+
for (const { name, line } of serverKeys) {
|
|
44601
|
+
const next = (counts.get(name) ?? 0) + 1;
|
|
44602
|
+
counts.set(name, next);
|
|
44603
|
+
if (next === 2) secondLines.set(name, line);
|
|
44604
|
+
}
|
|
44586
44605
|
for (const [name, count] of counts) {
|
|
44587
44606
|
if (count > 1) {
|
|
44588
44607
|
issues.push({
|
|
44589
44608
|
severity: "warning",
|
|
44590
44609
|
check: "mcp-consistency",
|
|
44591
44610
|
ruleId: "mcp-consistency/duplicate-server-name",
|
|
44592
|
-
line: 1,
|
|
44611
|
+
line: secondLines.get(name) ?? 1,
|
|
44593
44612
|
message: `Duplicate server name "${name}" in ${config2.relativePath} \u2014 only the last definition is used`
|
|
44594
44613
|
});
|
|
44595
44614
|
}
|
|
@@ -44748,6 +44767,17 @@ async function checkMcphApibase(config2, _projectRoot) {
|
|
|
44748
44767
|
});
|
|
44749
44768
|
return issues;
|
|
44750
44769
|
}
|
|
44770
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
44771
|
+
issues.push({
|
|
44772
|
+
severity: "error",
|
|
44773
|
+
check: "mcph-apibase",
|
|
44774
|
+
ruleId: "mcph-apibase/invalid-apibase",
|
|
44775
|
+
line: pos.line,
|
|
44776
|
+
message: `"apiBase" must be an http(s) URL, got ${parsed.protocol.replace(/:$/, "")}`,
|
|
44777
|
+
suggestion: `Use an absolute http(s) URL, e.g. "https://mcp.hosting".`
|
|
44778
|
+
});
|
|
44779
|
+
return issues;
|
|
44780
|
+
}
|
|
44751
44781
|
if (parsed.protocol === "http:" && !isPrivateHost(parsed.hostname)) {
|
|
44752
44782
|
issues.push({
|
|
44753
44783
|
severity: "warning",
|
|
@@ -44941,6 +44971,12 @@ function extractPathsClassified(content) {
|
|
|
44941
44971
|
candidates.push(p2);
|
|
44942
44972
|
}
|
|
44943
44973
|
}
|
|
44974
|
+
for (const match of content.matchAll(DRIVE_ABS_PATH)) {
|
|
44975
|
+
const p2 = match[1].replace(/[)}\]]+$/, "");
|
|
44976
|
+
if (p2.length > 2) {
|
|
44977
|
+
candidates.push(p2);
|
|
44978
|
+
}
|
|
44979
|
+
}
|
|
44944
44980
|
const seen = /* @__PURE__ */ new Set();
|
|
44945
44981
|
const out = [];
|
|
44946
44982
|
for (const value of candidates) {
|
|
@@ -44993,7 +45029,7 @@ async function parseMemoryFile(filePath, projectDir) {
|
|
|
44993
45029
|
referencedPaths
|
|
44994
45030
|
};
|
|
44995
45031
|
}
|
|
44996
|
-
var WEB_FIRST_SEGMENTS, PATH_PATTERN2, BARE_FILE_PATH;
|
|
45032
|
+
var WEB_FIRST_SEGMENTS, PATH_PATTERN2, BARE_FILE_PATH, DRIVE_ABS_PATH;
|
|
44997
45033
|
var init_session_parser = __esm({
|
|
44998
45034
|
"src/core/session-parser.ts"() {
|
|
44999
45035
|
"use strict";
|
|
@@ -45002,6 +45038,7 @@ var init_session_parser = __esm({
|
|
|
45002
45038
|
WEB_FIRST_SEGMENTS = loadWebFirstSegments();
|
|
45003
45039
|
PATH_PATTERN2 = /(?:^|\s|['"`(])([.~/][^\s'"`),;:!?]+)/g;
|
|
45004
45040
|
BARE_FILE_PATH = /(?:^|[\s`"'(])([\w][\w-]*(?:\/[\w.-]+)+\.[a-zA-Z0-9]{1,8})\b/g;
|
|
45041
|
+
DRIVE_ABS_PATH = /(?:^|[\s`"'(])([A-Za-z]:[\\/][^\s'"`),;!?]+)/g;
|
|
45005
45042
|
}
|
|
45006
45043
|
});
|
|
45007
45044
|
|
|
@@ -45201,7 +45238,12 @@ var init_session_scanner = __esm({
|
|
|
45201
45238
|
},
|
|
45202
45239
|
{
|
|
45203
45240
|
provider: "goose",
|
|
45204
|
-
|
|
45241
|
+
// On win32 the goose dir lives under %APPDATA%. If APPDATA is unset,
|
|
45242
|
+
// `join('', 'Block', 'goose')` yields the cwd-relative `Block/goose`,
|
|
45243
|
+
// which the HOME guard in `detectProviders` does NOT catch (HOME/USERPROFILE
|
|
45244
|
+
// may still be set). Leave the dir empty so `existsSync('')` is always false
|
|
45245
|
+
// and we never match a cwd-relative `Block/goose`, mirroring the HOME-guard intent.
|
|
45246
|
+
dir: process.platform === "win32" ? process.env.APPDATA ? join5(process.env.APPDATA, "Block", "goose") : "" : join5(home, ".config", "goose")
|
|
45205
45247
|
},
|
|
45206
45248
|
{ provider: "continue", dir: join5(home, ".continue") },
|
|
45207
45249
|
{
|
|
@@ -45217,15 +45259,45 @@ import { resolve as resolve6, basename as basename3 } from "node:path";
|
|
|
45217
45259
|
function normalizePath2(p2) {
|
|
45218
45260
|
return resolve6(p2).replace(/\\/g, "/").toLowerCase();
|
|
45219
45261
|
}
|
|
45262
|
+
function extractSecretName(display) {
|
|
45263
|
+
const prefixMatch = display.match(SECRET_SET_PREFIX);
|
|
45264
|
+
if (!prefixMatch) return void 0;
|
|
45265
|
+
const rest = display.slice(prefixMatch.index + prefixMatch[0].length);
|
|
45266
|
+
const tokens = rest.split(/\s+/).filter((t2) => t2.length > 0);
|
|
45267
|
+
for (let i2 = 0; i2 < tokens.length; i2++) {
|
|
45268
|
+
const tok = tokens[i2];
|
|
45269
|
+
if (!tok.startsWith("-")) return tok;
|
|
45270
|
+
if (VALUE_FLAGS.has(tok) && !tok.includes("=")) i2++;
|
|
45271
|
+
}
|
|
45272
|
+
return void 0;
|
|
45273
|
+
}
|
|
45274
|
+
function repoBasename(repoSpec) {
|
|
45275
|
+
return (repoSpec.split("/").pop() || repoSpec).toLowerCase();
|
|
45276
|
+
}
|
|
45277
|
+
function repoMatchesSibling(repoSpec, sib) {
|
|
45278
|
+
const slash = repoSpec.indexOf("/");
|
|
45279
|
+
const sibBase = basename3(normalizePath2(sib.path));
|
|
45280
|
+
if (slash === -1) {
|
|
45281
|
+
return repoBasename(repoSpec) === sibBase;
|
|
45282
|
+
}
|
|
45283
|
+
const owner = repoSpec.slice(0, slash).toLowerCase();
|
|
45284
|
+
const repoName = repoSpec.slice(slash + 1).toLowerCase();
|
|
45285
|
+
if (repoName !== sibBase) return false;
|
|
45286
|
+
if (sib.gitOrg) return sib.gitOrg.toLowerCase() === owner;
|
|
45287
|
+
if (sib.gitRemoteUrl) {
|
|
45288
|
+
return sib.gitRemoteUrl.toLowerCase().includes(`${owner}/${repoName}`);
|
|
45289
|
+
}
|
|
45290
|
+
return true;
|
|
45291
|
+
}
|
|
45220
45292
|
async function checkMissingSecret(ctx) {
|
|
45221
45293
|
const issues = [];
|
|
45222
45294
|
const secrets = [];
|
|
45223
45295
|
for (const entry of ctx.history) {
|
|
45224
|
-
const
|
|
45225
|
-
if (!
|
|
45296
|
+
const name = extractSecretName(entry.display);
|
|
45297
|
+
if (!name) continue;
|
|
45226
45298
|
const repoMatch = entry.display.match(REPO_FLAG_PATTERN);
|
|
45227
45299
|
secrets.push({
|
|
45228
|
-
name
|
|
45300
|
+
name,
|
|
45229
45301
|
repo: repoMatch ? repoMatch[1] : void 0,
|
|
45230
45302
|
project: entry.project
|
|
45231
45303
|
});
|
|
@@ -45233,28 +45305,22 @@ async function checkMissingSecret(ctx) {
|
|
|
45233
45305
|
if (secrets.length === 0) return issues;
|
|
45234
45306
|
const byName = /* @__PURE__ */ new Map();
|
|
45235
45307
|
for (const s of secrets) {
|
|
45236
|
-
if (!byName.has(s.name)) byName.set(s.name, /* @__PURE__ */ new Set());
|
|
45237
|
-
byName.get(s.name)
|
|
45238
|
-
|
|
45308
|
+
if (!byName.has(s.name)) byName.set(s.name, { projects: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set() });
|
|
45309
|
+
const bucket = byName.get(s.name);
|
|
45310
|
+
bucket.projects.add(s.project);
|
|
45311
|
+
if (s.repo) bucket.repos.add(s.repo);
|
|
45239
45312
|
}
|
|
45240
45313
|
const currentNorm = normalizePath2(ctx.currentProject);
|
|
45241
45314
|
const currentBase = basename3(currentNorm);
|
|
45242
|
-
for (const [secretName, projects] of byName) {
|
|
45315
|
+
for (const [secretName, { projects, repos }] of byName) {
|
|
45243
45316
|
const normalizedProjects = [...projects].map(normalizePath2);
|
|
45244
|
-
const
|
|
45245
|
-
|
|
45246
|
-
const lastSegment = p2.split("/").pop() || "";
|
|
45247
|
-
return lastSegment === currentBase;
|
|
45248
|
-
});
|
|
45317
|
+
const repoSpecs = [...repos];
|
|
45318
|
+
const currentHas = normalizedProjects.some((p2) => p2 === currentNorm) || repoSpecs.some((r2) => repoBasename(r2) === currentBase);
|
|
45249
45319
|
if (currentHas) continue;
|
|
45250
45320
|
const siblingMatches = ctx.siblings.filter((sib) => {
|
|
45251
45321
|
const sibNorm = normalizePath2(sib.path);
|
|
45252
|
-
|
|
45253
|
-
return
|
|
45254
|
-
if (p2 === sibNorm) return true;
|
|
45255
|
-
const lastSegment = p2.split("/").pop() || "";
|
|
45256
|
-
return lastSegment === sibBase;
|
|
45257
|
-
});
|
|
45322
|
+
if (normalizedProjects.some((p2) => p2 === sibNorm)) return true;
|
|
45323
|
+
return repoSpecs.some((r2) => repoMatchesSibling(r2, sib));
|
|
45258
45324
|
});
|
|
45259
45325
|
if (siblingMatches.length >= 2) {
|
|
45260
45326
|
const sibNames = siblingMatches.map((s) => s.name).join(", ");
|
|
@@ -45271,13 +45337,14 @@ async function checkMissingSecret(ctx) {
|
|
|
45271
45337
|
}
|
|
45272
45338
|
return issues;
|
|
45273
45339
|
}
|
|
45274
|
-
var
|
|
45340
|
+
var SECRET_SET_PREFIX, REPO_FLAG_PATTERN, VALUE_FLAGS;
|
|
45275
45341
|
var init_missing_secret = __esm({
|
|
45276
45342
|
"src/core/checks/session/missing-secret.ts"() {
|
|
45277
45343
|
"use strict";
|
|
45278
45344
|
init_define_WEB_FIRST_SEGMENTS();
|
|
45279
|
-
|
|
45345
|
+
SECRET_SET_PREFIX = /gh\s+secret\s+set\s+/;
|
|
45280
45346
|
REPO_FLAG_PATTERN = /--repo\s+(\S+)/;
|
|
45347
|
+
VALUE_FLAGS = /* @__PURE__ */ new Set(["--repo", "-R", "-b", "--body", "--app", "--env"]);
|
|
45281
45348
|
}
|
|
45282
45349
|
});
|
|
45283
45350
|
|
|
@@ -45438,14 +45505,14 @@ var init_missing_workflow = __esm({
|
|
|
45438
45505
|
|
|
45439
45506
|
// src/core/checks/session/stale-memory.ts
|
|
45440
45507
|
import { existsSync as existsSync4 } from "node:fs";
|
|
45441
|
-
import { resolve as resolve8, isAbsolute as
|
|
45508
|
+
import { resolve as resolve8, isAbsolute as isAbsolute3 } from "node:path";
|
|
45442
45509
|
function resolveRef2(ref, projectRoot) {
|
|
45443
45510
|
if (ref.startsWith("~/") || ref === "~") {
|
|
45444
45511
|
const home2 = process.env.HOME || process.env.USERPROFILE;
|
|
45445
45512
|
if (!home2) return null;
|
|
45446
45513
|
return ref === "~" ? home2 : resolve8(home2, ref.slice(2));
|
|
45447
45514
|
}
|
|
45448
|
-
if (
|
|
45515
|
+
if (isAbsolute3(ref)) return ref;
|
|
45449
45516
|
return resolve8(projectRoot, ref);
|
|
45450
45517
|
}
|
|
45451
45518
|
async function checkStaleMemory(ctx) {
|
|
@@ -45470,6 +45537,7 @@ async function checkStaleMemory(ctx) {
|
|
|
45470
45537
|
ruleId: "session-stale-memory/stale-memory",
|
|
45471
45538
|
line: 0,
|
|
45472
45539
|
message: `Memory "${name}" references ${brokenPaths.length} path(s) that no longer exist: ${brokenPaths.join(", ")}`,
|
|
45540
|
+
affectedPaths: brokenPaths,
|
|
45473
45541
|
suggestion: `Update or remove the memory file: ${mem.filePath}`,
|
|
45474
45542
|
detail: `Memory files with broken path references may cause the AI agent to follow stale instructions`
|
|
45475
45543
|
});
|
|
@@ -45663,7 +45731,7 @@ async function checkMemoryIndexOverflow(ctx) {
|
|
|
45663
45731
|
issues.push({
|
|
45664
45732
|
severity: "warning",
|
|
45665
45733
|
check: "session-memory-index-overflow",
|
|
45666
|
-
ruleId: "session-memory-index-overflow/
|
|
45734
|
+
ruleId: "session-memory-index-overflow/line-overflow",
|
|
45667
45735
|
line: MAX_LINES + 1,
|
|
45668
45736
|
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.`,
|
|
45669
45737
|
detail: `File: ${memoryFile}`,
|
|
@@ -45675,7 +45743,7 @@ async function checkMemoryIndexOverflow(ctx) {
|
|
|
45675
45743
|
issues.push({
|
|
45676
45744
|
severity: "warning",
|
|
45677
45745
|
check: "session-memory-index-overflow",
|
|
45678
|
-
ruleId: "session-memory-index-overflow/
|
|
45746
|
+
ruleId: "session-memory-index-overflow/byte-overflow",
|
|
45679
45747
|
line: 0,
|
|
45680
45748
|
message: `MEMORY.md is ${byteSize.toLocaleString()} bytes \u2014 only the first ${MAX_BYTES.toLocaleString()} bytes are loaded. ~${excess.toLocaleString()} bytes are effectively invisible.`,
|
|
45681
45749
|
detail: `File: ${memoryFile}`,
|
|
@@ -46585,7 +46653,7 @@ function applyIgnoreRules(issues, rules) {
|
|
|
46585
46653
|
if (rule.check !== issue2.check) continue;
|
|
46586
46654
|
if (rule.pathPattern) {
|
|
46587
46655
|
if (issue2.check !== "session-stale-memory") continue;
|
|
46588
|
-
const paths = extractPathsFromMessage(issue2.message);
|
|
46656
|
+
const paths = issue2.affectedPaths ?? extractPathsFromMessage(issue2.message);
|
|
46589
46657
|
if (paths.length === 0) continue;
|
|
46590
46658
|
const allMatch = paths.every((p2) => rule.pathPattern.test(p2));
|
|
46591
46659
|
if (!allMatch) continue;
|
|
@@ -46643,7 +46711,7 @@ import { readFileSync as readFileSync7 } from "node:fs";
|
|
|
46643
46711
|
import { resolve as resolve12, dirname as dirname6 } from "node:path";
|
|
46644
46712
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
46645
46713
|
function loadVersion() {
|
|
46646
|
-
if (true) return "0.13.
|
|
46714
|
+
if (true) return "0.13.2";
|
|
46647
46715
|
const __dir = dirname6(fileURLToPath2(import.meta.url));
|
|
46648
46716
|
const pkgPath = resolve12(__dir, "../package.json");
|
|
46649
46717
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
@@ -46698,7 +46766,9 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
|
|
|
46698
46766
|
if (activeChecks.includes("tokens"))
|
|
46699
46767
|
checkPromises.push(checkTokens(file2, projectRoot, thresholds));
|
|
46700
46768
|
if (activeChecks.includes("tier-tokens"))
|
|
46701
|
-
checkPromises.push(
|
|
46769
|
+
checkPromises.push(
|
|
46770
|
+
checkTierTokens(file2, projectRoot, thresholds, Boolean(options.hooksGlobal))
|
|
46771
|
+
);
|
|
46702
46772
|
if (activeChecks.includes("redundancy"))
|
|
46703
46773
|
checkPromises.push(checkRedundancy(file2, projectRoot));
|
|
46704
46774
|
if (activeChecks.includes("frontmatter"))
|
|
@@ -46973,8 +47043,9 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
|
|
|
46973
47043
|
if (typeof issue2.wastedTokens === "number") {
|
|
46974
47044
|
estimatedWaste += issue2.wastedTokens;
|
|
46975
47045
|
} else if (issue2.suggestion) {
|
|
46976
|
-
const
|
|
46977
|
-
|
|
47046
|
+
for (const m of issue2.suggestion.matchAll(/~(\d+)\s+tokens/g)) {
|
|
47047
|
+
estimatedWaste += parseInt(m[1], 10);
|
|
47048
|
+
}
|
|
46978
47049
|
}
|
|
46979
47050
|
}
|
|
46980
47051
|
}
|
|
@@ -47652,12 +47723,6 @@ function applyFixes(result, options = {}) {
|
|
|
47652
47723
|
} : console.log.bind(console);
|
|
47653
47724
|
const dryRun = options.dryRun ?? false;
|
|
47654
47725
|
const skipSymlinks = options.skipSymlinks ?? true;
|
|
47655
|
-
const symlinkFiles = /* @__PURE__ */ new Set();
|
|
47656
|
-
if (skipSymlinks) {
|
|
47657
|
-
for (const f of result.files) {
|
|
47658
|
-
if (f.isSymlink) symlinkFiles.add(f.path);
|
|
47659
|
-
}
|
|
47660
|
-
}
|
|
47661
47726
|
const fixesByFile = /* @__PURE__ */ new Map();
|
|
47662
47727
|
const dedupeKeys = /* @__PURE__ */ new Map();
|
|
47663
47728
|
const skippedSymlinks = /* @__PURE__ */ new Set();
|
|
@@ -48213,7 +48278,7 @@ var init_server3 = __esm({
|
|
|
48213
48278
|
checks: external_exports3.array(mcphCheckEnum).optional().describe("Specific mcph checks to run (default: all mcph-* checks)."),
|
|
48214
48279
|
includeGlobal: external_exports3.boolean().optional().describe("Also scan ~/.mcph.json (user-global config)."),
|
|
48215
48280
|
strictEnvToken: external_exports3.boolean().optional().describe(
|
|
48216
|
-
"Upgrade mcph-
|
|
48281
|
+
"Upgrade mcph-token-security/prefer-env-token from warning to error (env-var-only posture)."
|
|
48217
48282
|
)
|
|
48218
48283
|
},
|
|
48219
48284
|
{
|
|
@@ -54960,12 +55025,13 @@ function formatTokenReport(result) {
|
|
|
54960
55025
|
source_default.dim(" (counts use GPT-4 cl100k_base tokenizer \u2014 Claude counts may vary slightly)")
|
|
54961
55026
|
);
|
|
54962
55027
|
lines.push("");
|
|
54963
|
-
const
|
|
55028
|
+
const realFiles = result.files.filter((f) => !isSyntheticBucket(f.path));
|
|
55029
|
+
const maxPathLen = Math.max(...realFiles.map((f) => f.path.length), 4);
|
|
54964
55030
|
lines.push(
|
|
54965
55031
|
` ${source_default.dim("File".padEnd(maxPathLen))} ${source_default.dim("Tokens".padStart(8))} ${source_default.dim("Lines".padStart(6))}`
|
|
54966
55032
|
);
|
|
54967
55033
|
lines.push(` ${"-".repeat(maxPathLen)} ${"-".repeat(8)} ${"-".repeat(6)}`);
|
|
54968
|
-
for (const file2 of
|
|
55034
|
+
for (const file2 of realFiles) {
|
|
54969
55035
|
const tokenStr = file2.tokens.toLocaleString().padStart(8);
|
|
54970
55036
|
const lineStr = file2.lines.toString().padStart(6);
|
|
54971
55037
|
lines.push(` ${file2.path.padEnd(maxPathLen)} ${tokenStr} ${lineStr}`);
|
|
@@ -54986,7 +55052,7 @@ function formatTokenReport(result) {
|
|
|
54986
55052
|
return lines.join("\n");
|
|
54987
55053
|
}
|
|
54988
55054
|
function isSyntheticPath(p2) {
|
|
54989
|
-
return p2
|
|
55055
|
+
return isSyntheticBucket(p2) || p2.startsWith("~");
|
|
54990
55056
|
}
|
|
54991
55057
|
function formatSarif(result) {
|
|
54992
55058
|
const severityToLevel = {
|
|
@@ -55308,7 +55374,7 @@ async function runCli() {
|
|
|
55308
55374
|
new Option("--format <format>", "Output format: text, json, or sarif").choices(["text", "json", "sarif"]).default("text")
|
|
55309
55375
|
).option("--tokens", "Show token breakdown per file", false).option("--verbose", "Show passing checks too", false).option("--fix", "Auto-fix broken paths using git history and fuzzy matching", false).option("--fix-dry-run", "Preview --fix changes without writing", false).option("--yes", "Skip interactive confirmation prompts (required for --fix in TTY)", false).option("--follow-symlinks", "Allow --fix to write through symlinks (default: skip)", false).option("--ignore <checks>", "Comma-separated list of checks to ignore", "").option("--quiet", "Suppress all output except errors (exit code only)", false).option("--config <path>", "Path to config file (default: .ctxlintrc in project root)").option("--depth <n>", "Max subdirectory depth to scan (default: 2)", "2").option("--mcp", "Enable MCP config linting alongside context file checks", false).option("--mcp-only", "Run only MCP config checks, skip context file checks", false).option("--mcp-global", "Also scan user/global MCP config files (implies --mcp)", false).option("--mcp-server", "Start the MCP server (alias: `ctxlint serve`)").option("--mcph", "Enable .mcph.json (mcp.hosting CLI config) linting", false).option("--mcph-only", "Run only mcph config checks", false).option("--mcph-global", "Also scan ~/.mcph.json (implies --mcph)", false).option(
|
|
55310
55376
|
"--mcph-strict-env-token",
|
|
55311
|
-
"Upgrade mcph-
|
|
55377
|
+
"Upgrade mcph-token-security/prefer-env-token from warning to error (strict env-var-only posture)",
|
|
55312
55378
|
false
|
|
55313
55379
|
).option("--session", "Run session audit checks (cross-project consistency)", false).option("--session-only", "Run only session checks, skip context and MCP checks", false).option("--skills", "Run agent-skill checks (~/.claude/skills + ~/.claude/agents)", false).option("--skills-only", "Run only agent-skill checks, skip everything else", false).option(
|
|
55314
55380
|
"--hooks-global",
|
|
@@ -55342,7 +55408,7 @@ async function runCli() {
|
|
|
55342
55408
|
if (result.files.length === 0) {
|
|
55343
55409
|
if (!options.quiet) {
|
|
55344
55410
|
if (options.format === "json") {
|
|
55345
|
-
console.log(
|
|
55411
|
+
console.log(formatJson(result));
|
|
55346
55412
|
} else if (options.format === "sarif") {
|
|
55347
55413
|
console.log(formatSarif(result));
|
|
55348
55414
|
} else {
|
|
@@ -55482,7 +55548,7 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
55482
55548
|
let liveOptions = options;
|
|
55483
55549
|
let liveActiveChecks = activeChecks;
|
|
55484
55550
|
try {
|
|
55485
|
-
const resolved = resolveSession(resolvedPath, opts);
|
|
55551
|
+
const resolved = resolveSession(resolvedPath, opts, true);
|
|
55486
55552
|
liveConfig = resolved.config;
|
|
55487
55553
|
liveOptions = resolved.options;
|
|
55488
55554
|
liveActiveChecks = resolved.activeChecks;
|
|
@@ -55604,9 +55670,9 @@ async function promptYesNo(question) {
|
|
|
55604
55670
|
rl.close();
|
|
55605
55671
|
}
|
|
55606
55672
|
}
|
|
55607
|
-
function resolveSession(resolvedPath, opts) {
|
|
55673
|
+
function resolveSession(resolvedPath, opts, throwOnConfigError = false) {
|
|
55608
55674
|
const configPath = opts.config ? path16.resolve(opts.config) : void 0;
|
|
55609
|
-
const config2 = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
|
|
55675
|
+
const config2 = configPath ? loadConfigFromPath(configPath, throwOnConfigError) : loadConfig(resolvedPath);
|
|
55610
55676
|
const mcpGlobal = opts.mcpGlobal || config2?.mcpGlobal || false;
|
|
55611
55677
|
const mcpOnly = opts.mcpOnly || config2?.mcpOnly || false;
|
|
55612
55678
|
const mcpFlag = opts.mcp || mcpGlobal || mcpOnly || config2?.mcp || false;
|
|
@@ -55683,10 +55749,11 @@ function resolveSession(resolvedPath, opts) {
|
|
|
55683
55749
|
const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
|
|
55684
55750
|
return { config: config2, options, activeChecks };
|
|
55685
55751
|
}
|
|
55686
|
-
function loadConfigFromPath(configPath) {
|
|
55752
|
+
function loadConfigFromPath(configPath, throwOnError = false) {
|
|
55687
55753
|
try {
|
|
55688
55754
|
return loadConfigFromExplicitPath(configPath);
|
|
55689
55755
|
} catch (err) {
|
|
55756
|
+
if (throwOnError) throw err;
|
|
55690
55757
|
const detail = err instanceof Error ? err.message : String(err);
|
|
55691
55758
|
console.error(`Error: ${detail}`);
|
|
55692
55759
|
process.exit(2);
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"rules": [
|
|
37
37
|
{
|
|
38
|
-
"id": "mcph-
|
|
38
|
+
"id": "mcph-token-security/token-in-project-scope",
|
|
39
39
|
"category": "mcph-token-security",
|
|
40
40
|
"severity": "error",
|
|
41
41
|
"description": "A project-scope .mcph.json that is git-tracked contains a \"token\" field. The PAT will leak via git history.",
|
|
@@ -46,18 +46,18 @@
|
|
|
46
46
|
"stability": "stable"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
|
-
"id": "mcph-
|
|
49
|
+
"id": "mcph-gitignore/local-file-not-gitignored",
|
|
50
50
|
"category": "mcph-gitignore",
|
|
51
51
|
"severity": "error",
|
|
52
52
|
"description": "A .mcph.local.json file exists but is not covered by .gitignore. The file exists precisely so machine-local overrides stay machine-local.",
|
|
53
53
|
"trigger": "scope === 'project-local' && !isGitignored",
|
|
54
54
|
"message": "{file} is not covered by .gitignore — machine-local overrides can leak via git",
|
|
55
|
-
"fixable":
|
|
55
|
+
"fixable": false,
|
|
56
56
|
"fixDescription": "Append \".mcph.local.json\" to .gitignore in the project root.",
|
|
57
57
|
"stability": "stable"
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
|
-
"id": "mcph-
|
|
60
|
+
"id": "mcph-token-security/invalid-token-format",
|
|
61
61
|
"category": "mcph-token-security",
|
|
62
62
|
"severity": "error",
|
|
63
63
|
"description": "The \"token\" value doesn't match the mcp.hosting PAT format (^mcp_pat_[A-Za-z0-9_-]+$).",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"stability": "stable"
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
|
-
"id": "mcph-
|
|
71
|
+
"id": "mcph-apibase/insecure-apibase",
|
|
72
72
|
"category": "mcph-apibase",
|
|
73
73
|
"severity": "warning",
|
|
74
74
|
"description": "The \"apiBase\" URL uses plaintext HTTP to a public (non-localhost, non-private) host. The MCPH_TOKEN would travel in the clear.",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"stability": "stable"
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
|
-
"id": "mcph-
|
|
82
|
+
"id": "mcph-apibase/invalid-apibase",
|
|
83
83
|
"category": "mcph-apibase",
|
|
84
84
|
"severity": "error",
|
|
85
85
|
"description": "The \"apiBase\" field is not a valid absolute URL.",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"stability": "stable"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
|
-
"id": "mcph-
|
|
93
|
+
"id": "mcph-schema-conformance/unknown-field",
|
|
94
94
|
"category": "mcph-schema-conformance",
|
|
95
95
|
"severity": "warning",
|
|
96
96
|
"description": "A top-level field appears that is not in the mcph config schema. Usually a typo (tokens vs token, blockList vs blocked).",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"stability": "stable"
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
|
-
"id": "mcph-
|
|
104
|
+
"id": "mcph-schema-conformance/stale-version",
|
|
105
105
|
"category": "mcph-schema-conformance",
|
|
106
106
|
"severity": "info",
|
|
107
107
|
"description": "The \"version\" field is older than the current schema version.",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"stability": "stable"
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
|
-
"id": "mcph-
|
|
115
|
+
"id": "mcph-lists/allowlist-denylist-conflict",
|
|
116
116
|
"category": "mcph-lists",
|
|
117
117
|
"severity": "warning",
|
|
118
118
|
"description": "A server namespace appears in both the allow-list (\"servers\") and deny-list (\"blocked\"). Deny wins in practice, so the allow-list entry is dead weight.",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"stability": "stable"
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
|
-
"id": "mcph-
|
|
126
|
+
"id": "mcph-lists/duplicate-entries",
|
|
127
127
|
"category": "mcph-lists",
|
|
128
128
|
"severity": "info",
|
|
129
129
|
"description": "The same value appears more than once in a servers or blocked array.",
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"stability": "stable"
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
|
-
"id": "mcph-
|
|
137
|
+
"id": "mcph-token-security/prefer-env-token",
|
|
138
138
|
"category": "mcph-token-security",
|
|
139
139
|
"severity": "warning",
|
|
140
140
|
"configurable": true,
|
package/package.json
CHANGED