@yawlabs/ctxlint 0.13.1 → 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 +266 -135
- package/mcph-config-lint-rules.json +11 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22404,7 +22404,7 @@ var init_protocol = __esm({
|
|
|
22404
22404
|
return;
|
|
22405
22405
|
}
|
|
22406
22406
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
22407
|
-
await new Promise((
|
|
22407
|
+
await new Promise((resolve15) => setTimeout(resolve15, pollInterval));
|
|
22408
22408
|
options?.signal?.throwIfAborted();
|
|
22409
22409
|
}
|
|
22410
22410
|
} catch (error49) {
|
|
@@ -22421,7 +22421,7 @@ var init_protocol = __esm({
|
|
|
22421
22421
|
*/
|
|
22422
22422
|
request(request, resultSchema, options) {
|
|
22423
22423
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
22424
|
-
return new Promise((
|
|
22424
|
+
return new Promise((resolve15, reject) => {
|
|
22425
22425
|
const earlyReject = (error49) => {
|
|
22426
22426
|
reject(error49);
|
|
22427
22427
|
};
|
|
@@ -22499,7 +22499,7 @@ var init_protocol = __esm({
|
|
|
22499
22499
|
if (!parseResult.success) {
|
|
22500
22500
|
reject(parseResult.error);
|
|
22501
22501
|
} else {
|
|
22502
|
-
|
|
22502
|
+
resolve15(parseResult.data);
|
|
22503
22503
|
}
|
|
22504
22504
|
} catch (error49) {
|
|
22505
22505
|
reject(error49);
|
|
@@ -22760,12 +22760,12 @@ var init_protocol = __esm({
|
|
|
22760
22760
|
}
|
|
22761
22761
|
} catch {
|
|
22762
22762
|
}
|
|
22763
|
-
return new Promise((
|
|
22763
|
+
return new Promise((resolve15, reject) => {
|
|
22764
22764
|
if (signal.aborted) {
|
|
22765
22765
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
22766
22766
|
return;
|
|
22767
22767
|
}
|
|
22768
|
-
const timeoutId = setTimeout(
|
|
22768
|
+
const timeoutId = setTimeout(resolve15, interval);
|
|
22769
22769
|
signal.addEventListener("abort", () => {
|
|
22770
22770
|
clearTimeout(timeoutId);
|
|
22771
22771
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -25813,7 +25813,7 @@ var require_compile = __commonJS({
|
|
|
25813
25813
|
const schOrFunc = root.refs[ref];
|
|
25814
25814
|
if (schOrFunc)
|
|
25815
25815
|
return schOrFunc;
|
|
25816
|
-
let _sch =
|
|
25816
|
+
let _sch = resolve15.call(this, root, ref);
|
|
25817
25817
|
if (_sch === void 0) {
|
|
25818
25818
|
const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
25819
25819
|
const { schemaId } = this.opts;
|
|
@@ -25840,7 +25840,7 @@ var require_compile = __commonJS({
|
|
|
25840
25840
|
function sameSchemaEnv(s1, s2) {
|
|
25841
25841
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
25842
25842
|
}
|
|
25843
|
-
function
|
|
25843
|
+
function resolve15(root, ref) {
|
|
25844
25844
|
let sch;
|
|
25845
25845
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
25846
25846
|
ref = sch;
|
|
@@ -26418,7 +26418,7 @@ var require_fast_uri = __commonJS({
|
|
|
26418
26418
|
}
|
|
26419
26419
|
return uri;
|
|
26420
26420
|
}
|
|
26421
|
-
function
|
|
26421
|
+
function resolve15(baseURI, relativeURI, options) {
|
|
26422
26422
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
26423
26423
|
const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
26424
26424
|
schemelessOptions.skipEscape = true;
|
|
@@ -26645,7 +26645,7 @@ var require_fast_uri = __commonJS({
|
|
|
26645
26645
|
var fastUri = {
|
|
26646
26646
|
SCHEMES,
|
|
26647
26647
|
normalize: normalize3,
|
|
26648
|
-
resolve:
|
|
26648
|
+
resolve: resolve15,
|
|
26649
26649
|
resolveComponent,
|
|
26650
26650
|
equal,
|
|
26651
26651
|
serialize,
|
|
@@ -30827,7 +30827,7 @@ var init_mcp = __esm({
|
|
|
30827
30827
|
let task = createTaskResult.task;
|
|
30828
30828
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
30829
30829
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
30830
|
-
await new Promise((
|
|
30830
|
+
await new Promise((resolve15) => setTimeout(resolve15, pollInterval));
|
|
30831
30831
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
30832
30832
|
if (!updatedTask) {
|
|
30833
30833
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -31423,12 +31423,12 @@ var init_stdio2 = __esm({
|
|
|
31423
31423
|
this.onclose?.();
|
|
31424
31424
|
}
|
|
31425
31425
|
send(message) {
|
|
31426
|
-
return new Promise((
|
|
31426
|
+
return new Promise((resolve15) => {
|
|
31427
31427
|
const json2 = serializeMessage(message);
|
|
31428
31428
|
if (this._stdout.write(json2)) {
|
|
31429
|
-
|
|
31429
|
+
resolve15();
|
|
31430
31430
|
} else {
|
|
31431
|
-
this._stdout.once("drain",
|
|
31431
|
+
this._stdout.once("drain", resolve15);
|
|
31432
31432
|
}
|
|
31433
31433
|
});
|
|
31434
31434
|
}
|
|
@@ -34433,19 +34433,28 @@ function stripBom(content) {
|
|
|
34433
34433
|
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
34434
34434
|
}
|
|
34435
34435
|
function loadPackageJson(projectRoot) {
|
|
34436
|
-
if (pkgJsonCache
|
|
34436
|
+
if (pkgJsonCache.has(projectRoot)) {
|
|
34437
|
+
const cached2 = pkgJsonCache.get(projectRoot) ?? null;
|
|
34438
|
+
pkgJsonCache.delete(projectRoot);
|
|
34439
|
+
pkgJsonCache.set(projectRoot, cached2);
|
|
34440
|
+
return cached2;
|
|
34441
|
+
}
|
|
34442
|
+
let data;
|
|
34437
34443
|
try {
|
|
34438
34444
|
const content = stripBom(fs2.readFileSync(path.join(projectRoot, "package.json"), "utf-8"));
|
|
34439
|
-
|
|
34440
|
-
pkgJsonCache = { root: projectRoot, data };
|
|
34441
|
-
return data;
|
|
34445
|
+
data = JSON.parse(content);
|
|
34442
34446
|
} catch {
|
|
34443
|
-
|
|
34444
|
-
return null;
|
|
34447
|
+
data = null;
|
|
34445
34448
|
}
|
|
34449
|
+
pkgJsonCache.set(projectRoot, data);
|
|
34450
|
+
if (pkgJsonCache.size > PKG_CACHE_MAX_ENTRIES) {
|
|
34451
|
+
const oldest = pkgJsonCache.keys().next().value;
|
|
34452
|
+
if (oldest !== void 0) pkgJsonCache.delete(oldest);
|
|
34453
|
+
}
|
|
34454
|
+
return data;
|
|
34446
34455
|
}
|
|
34447
34456
|
function resetPackageJsonCache() {
|
|
34448
|
-
pkgJsonCache
|
|
34457
|
+
pkgJsonCache.clear();
|
|
34449
34458
|
}
|
|
34450
34459
|
function fileExists(filePath) {
|
|
34451
34460
|
try {
|
|
@@ -34511,12 +34520,13 @@ function getAllProjectFiles(projectRoot) {
|
|
|
34511
34520
|
walk(projectRoot, 0);
|
|
34512
34521
|
return files;
|
|
34513
34522
|
}
|
|
34514
|
-
var pkgJsonCache, IGNORED_DIRS;
|
|
34523
|
+
var PKG_CACHE_MAX_ENTRIES, pkgJsonCache, IGNORED_DIRS;
|
|
34515
34524
|
var init_fs = __esm({
|
|
34516
34525
|
"src/utils/fs.ts"() {
|
|
34517
34526
|
"use strict";
|
|
34518
34527
|
init_define_WEB_FIRST_SEGMENTS();
|
|
34519
|
-
|
|
34528
|
+
PKG_CACHE_MAX_ENTRIES = 256;
|
|
34529
|
+
pkgJsonCache = /* @__PURE__ */ new Map();
|
|
34520
34530
|
IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
34521
34531
|
"node_modules",
|
|
34522
34532
|
".git",
|
|
@@ -40917,7 +40927,7 @@ function parseRenameLog(result, targetPath) {
|
|
|
40917
40927
|
let basenameFallback = null;
|
|
40918
40928
|
const lines = result.trim().split("\n");
|
|
40919
40929
|
for (const line of lines) {
|
|
40920
|
-
const headerMatch = line.match(/^([a-f0-9]{7,40})\s+(
|
|
40930
|
+
const headerMatch = line.match(/^([a-f0-9]{7,40})\s+(\d{4}-\d\d-\d\d\b.*)$/);
|
|
40921
40931
|
if (headerMatch) {
|
|
40922
40932
|
currentHash = headerMatch[1].substring(0, 7);
|
|
40923
40933
|
currentDateStr = headerMatch[2];
|
|
@@ -42479,7 +42489,7 @@ async function checkPaths(file2, projectRoot) {
|
|
|
42479
42489
|
const resolvedPath = path4.resolve(baseDir, ref.value);
|
|
42480
42490
|
const normalizedRef = ref.value.replace(/\\/g, "/");
|
|
42481
42491
|
if (normalizedRef.includes("*")) {
|
|
42482
|
-
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 });
|
|
42483
42493
|
if (matches.length === 0) {
|
|
42484
42494
|
issues.push({
|
|
42485
42495
|
severity: "error",
|
|
@@ -42603,10 +42613,26 @@ function extractNpxPackage(cmd) {
|
|
|
42603
42613
|
}
|
|
42604
42614
|
return null;
|
|
42605
42615
|
}
|
|
42616
|
+
function wouldNeedPackageJson(cmd) {
|
|
42617
|
+
return NPM_SCRIPT_PATTERN.test(cmd) || PKG_SHORTHAND_PATTERN.test(cmd) || /^npx\b/.test(cmd) || PKG_DEPENDENT_TOOL_PATTERN.test(cmd);
|
|
42618
|
+
}
|
|
42606
42619
|
async function checkCommands(file2, projectRoot) {
|
|
42607
42620
|
const issues = [];
|
|
42608
42621
|
const pkgJson = loadPackageJson(projectRoot);
|
|
42609
42622
|
const makefile = loadMakefile(projectRoot);
|
|
42623
|
+
if (!pkgJson) {
|
|
42624
|
+
const skipped = file2.references.commands.find((ref) => wouldNeedPackageJson(ref.value));
|
|
42625
|
+
if (skipped) {
|
|
42626
|
+
issues.push({
|
|
42627
|
+
severity: "info",
|
|
42628
|
+
check: "commands",
|
|
42629
|
+
ruleId: "commands/package-json-missing",
|
|
42630
|
+
line: skipped.line,
|
|
42631
|
+
message: "package.json missing or unparseable \u2014 command checks skipped",
|
|
42632
|
+
suggestion: "Add a parseable package.json at the project root so script, npx, and tool references can be validated."
|
|
42633
|
+
});
|
|
42634
|
+
}
|
|
42635
|
+
}
|
|
42610
42636
|
for (const ref of file2.references.commands) {
|
|
42611
42637
|
const cmd = ref.value;
|
|
42612
42638
|
const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
|
|
@@ -42625,9 +42651,7 @@ async function checkCommands(file2, projectRoot) {
|
|
|
42625
42651
|
}
|
|
42626
42652
|
continue;
|
|
42627
42653
|
}
|
|
42628
|
-
const shorthandMatch = cmd.match(
|
|
42629
|
-
/^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/
|
|
42630
|
-
);
|
|
42654
|
+
const shorthandMatch = cmd.match(PKG_SHORTHAND_PATTERN);
|
|
42631
42655
|
if (shorthandMatch && pkgJson) {
|
|
42632
42656
|
const scriptName = shorthandMatch[2];
|
|
42633
42657
|
if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
|
|
@@ -42689,7 +42713,7 @@ async function checkCommands(file2, projectRoot) {
|
|
|
42689
42713
|
}
|
|
42690
42714
|
continue;
|
|
42691
42715
|
}
|
|
42692
|
-
const toolMatch = cmd.match(
|
|
42716
|
+
const toolMatch = cmd.match(PKG_DEPENDENT_TOOL_PATTERN);
|
|
42693
42717
|
if (toolMatch && pkgJson) {
|
|
42694
42718
|
const tool = toolMatch[1];
|
|
42695
42719
|
const allDeps = {
|
|
@@ -42727,7 +42751,7 @@ function hasMakeTarget(makefile, target) {
|
|
|
42727
42751
|
const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
|
|
42728
42752
|
return pattern.test(makefile);
|
|
42729
42753
|
}
|
|
42730
|
-
var NPM_SCRIPT_PATTERN, MAKE_PATTERN;
|
|
42754
|
+
var NPM_SCRIPT_PATTERN, MAKE_PATTERN, PKG_DEPENDENT_TOOL_PATTERN, PKG_SHORTHAND_PATTERN;
|
|
42731
42755
|
var init_commands = __esm({
|
|
42732
42756
|
"src/core/checks/commands.ts"() {
|
|
42733
42757
|
"use strict";
|
|
@@ -42735,6 +42759,8 @@ var init_commands = __esm({
|
|
|
42735
42759
|
init_fs();
|
|
42736
42760
|
NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
|
|
42737
42761
|
MAKE_PATTERN = /^make\s+(\S+)/;
|
|
42762
|
+
PKG_DEPENDENT_TOOL_PATTERN = /^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/;
|
|
42763
|
+
PKG_SHORTHAND_PATTERN = /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/;
|
|
42738
42764
|
}
|
|
42739
42765
|
});
|
|
42740
42766
|
|
|
@@ -42845,7 +42871,7 @@ async function checkTokens(file2, _projectRoot, thresholds = DEFAULT_TOKEN_THRES
|
|
|
42845
42871
|
}
|
|
42846
42872
|
function checkAggregateTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
|
|
42847
42873
|
const total = files.reduce((sum, f) => sum + f.tokens, 0);
|
|
42848
|
-
if (total
|
|
42874
|
+
if (total >= thresholds.aggregate && files.length > 1) {
|
|
42849
42875
|
return {
|
|
42850
42876
|
severity: "warning",
|
|
42851
42877
|
check: "tokens",
|
|
@@ -42924,13 +42950,20 @@ function computeSectionCosts(file2) {
|
|
|
42924
42950
|
return { title: s.title, line: s.startLine, tokens: countTokens(body) };
|
|
42925
42951
|
}).sort((a, b2) => b2.tokens - a.tokens);
|
|
42926
42952
|
}
|
|
42927
|
-
function loadSettingsSources(projectRoot) {
|
|
42953
|
+
function loadSettingsSources(projectRoot, includeGlobal) {
|
|
42954
|
+
if (settingsCache?.root === projectRoot && settingsCache.includeGlobal === includeGlobal) {
|
|
42955
|
+
return settingsCache.data;
|
|
42956
|
+
}
|
|
42928
42957
|
const sources = [];
|
|
42929
42958
|
const candidates = [
|
|
42930
42959
|
path7.join(projectRoot, ".claude", "settings.json"),
|
|
42931
|
-
path7.join(projectRoot, ".claude", "settings.local.json")
|
|
42932
|
-
path7.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
|
|
42960
|
+
path7.join(projectRoot, ".claude", "settings.local.json")
|
|
42933
42961
|
];
|
|
42962
|
+
if (includeGlobal) {
|
|
42963
|
+
candidates.push(
|
|
42964
|
+
path7.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
|
|
42965
|
+
);
|
|
42966
|
+
}
|
|
42934
42967
|
for (const p2 of candidates) {
|
|
42935
42968
|
let content;
|
|
42936
42969
|
try {
|
|
@@ -42944,6 +42977,7 @@ function loadSettingsSources(projectRoot) {
|
|
|
42944
42977
|
console.warn(`ctxlint: could not parse ${p2}: ${err.message}`);
|
|
42945
42978
|
}
|
|
42946
42979
|
}
|
|
42980
|
+
settingsCache = { root: projectRoot, includeGlobal, data: sources };
|
|
42947
42981
|
return sources;
|
|
42948
42982
|
}
|
|
42949
42983
|
function canonicalizeCommand(backticked) {
|
|
@@ -42995,7 +43029,7 @@ function checkHardEnforcement(file2, settings) {
|
|
|
42995
43029
|
}
|
|
42996
43030
|
return issues;
|
|
42997
43031
|
}
|
|
42998
|
-
async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
|
|
43032
|
+
async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS, includeGlobal = false) {
|
|
42999
43033
|
if (!isAlwaysLoaded(file2)) return [];
|
|
43000
43034
|
const issues = [];
|
|
43001
43035
|
const threshold = thresholds.tierBreakdown;
|
|
@@ -43017,7 +43051,7 @@ async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_TH
|
|
|
43017
43051
|
});
|
|
43018
43052
|
}
|
|
43019
43053
|
}
|
|
43020
|
-
const settings = loadSettingsSources(projectRoot);
|
|
43054
|
+
const settings = loadSettingsSources(projectRoot, includeGlobal);
|
|
43021
43055
|
issues.push(...checkHardEnforcement(file2, settings));
|
|
43022
43056
|
return issues;
|
|
43023
43057
|
}
|
|
@@ -43038,7 +43072,7 @@ function checkAggregateTierTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS)
|
|
|
43038
43072
|
suggestion: "Consider moving the largest files or their heaviest sections to on-demand tiers (skills, subagents, memory)."
|
|
43039
43073
|
};
|
|
43040
43074
|
}
|
|
43041
|
-
var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
|
|
43075
|
+
var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND, settingsCache;
|
|
43042
43076
|
var init_tier_tokens = __esm({
|
|
43043
43077
|
"src/core/checks/tier-tokens.ts"() {
|
|
43044
43078
|
"use strict";
|
|
@@ -43068,6 +43102,7 @@ var init_tier_tokens = __esm({
|
|
|
43068
43102
|
];
|
|
43069
43103
|
TOP_SECTIONS_TO_REPORT = 3;
|
|
43070
43104
|
INVIOLABLE_WITH_COMMAND = /\b(NEVER|ALWAYS|DON'?T|DO NOT|MUST NOT)\b[^.!?`]{0,80}`([^`]+)`/i;
|
|
43105
|
+
settingsCache = null;
|
|
43071
43106
|
}
|
|
43072
43107
|
});
|
|
43073
43108
|
|
|
@@ -43164,7 +43199,8 @@ async function checkRedundancy(file2, projectRoot) {
|
|
|
43164
43199
|
ruleId: "redundancy/tech-mention",
|
|
43165
43200
|
line: i2 + 1,
|
|
43166
43201
|
message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : pkgJson.devDependencies?.[pkg] ? "devDependencies" : pkgJson.peerDependencies?.[pkg] ? "peerDependencies" : "optionalDependencies"} \u2014 agent can infer this`,
|
|
43167
|
-
suggestion: `~${wastedTokens} tokens could be saved
|
|
43202
|
+
suggestion: `~${wastedTokens} tokens could be saved`,
|
|
43203
|
+
wastedTokens
|
|
43168
43204
|
});
|
|
43169
43205
|
}
|
|
43170
43206
|
}
|
|
@@ -43178,7 +43214,8 @@ async function checkRedundancy(file2, projectRoot) {
|
|
|
43178
43214
|
);
|
|
43179
43215
|
if (dirMatch) {
|
|
43180
43216
|
const dir = dirMatch[1].replace(/[`"]/g, "");
|
|
43181
|
-
const
|
|
43217
|
+
const baseDir = dir.startsWith("./") || dir.startsWith("../") ? path8.dirname(file2.filePath) : projectRoot;
|
|
43218
|
+
const fullPath = path8.resolve(baseDir, dir);
|
|
43182
43219
|
if (isDirectory(fullPath)) {
|
|
43183
43220
|
issues.push({
|
|
43184
43221
|
severity: "info",
|
|
@@ -43410,19 +43447,23 @@ var init_contradictions = __esm({
|
|
|
43410
43447
|
options: [
|
|
43411
43448
|
{
|
|
43412
43449
|
label: "Jest",
|
|
43413
|
-
|
|
43450
|
+
// The third pattern bounds the gap between `test` and `with <fw>` to a
|
|
43451
|
+
// few intra-clause chars so prose like "run our test suite and then
|
|
43452
|
+
// deploy with jest" no longer matches the framework directive — only
|
|
43453
|
+
// tight phrasings like "test with jest" / "tests run with jest" do.
|
|
43454
|
+
patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest\w*\s+with\s+jest\b/i]
|
|
43414
43455
|
},
|
|
43415
43456
|
{
|
|
43416
43457
|
label: "Vitest",
|
|
43417
|
-
patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest
|
|
43458
|
+
patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest\w*\s+with\s+vitest\b/i]
|
|
43418
43459
|
},
|
|
43419
43460
|
{
|
|
43420
43461
|
label: "Mocha",
|
|
43421
|
-
patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest
|
|
43462
|
+
patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest\w*\s+with\s+mocha\b/i]
|
|
43422
43463
|
},
|
|
43423
43464
|
{
|
|
43424
43465
|
label: "pytest",
|
|
43425
|
-
patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest
|
|
43466
|
+
patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest\w*\s+with\s+pytest\b/i]
|
|
43426
43467
|
},
|
|
43427
43468
|
{
|
|
43428
43469
|
label: "Playwright",
|
|
@@ -43477,19 +43518,11 @@ var init_contradictions = __esm({
|
|
|
43477
43518
|
},
|
|
43478
43519
|
{
|
|
43479
43520
|
label: "2 spaces",
|
|
43480
|
-
patterns: [
|
|
43481
|
-
/\b2[\s-]?space\s+indent/i,
|
|
43482
|
-
/\bindent\s+with\s+2\s+spaces/i,
|
|
43483
|
-
/\b2[\s-]?space\s+tabs?\b/i
|
|
43484
|
-
]
|
|
43521
|
+
patterns: [/\b2[\s-]?space\s+indent/i, /\bindent\s+with\s+2\s+spaces/i]
|
|
43485
43522
|
},
|
|
43486
43523
|
{
|
|
43487
43524
|
label: "4 spaces",
|
|
43488
|
-
patterns: [
|
|
43489
|
-
/\b4[\s-]?space\s+indent/i,
|
|
43490
|
-
/\bindent\s+with\s+4\s+spaces/i,
|
|
43491
|
-
/\b4[\s-]?space\s+tabs?\b/i
|
|
43492
|
-
]
|
|
43525
|
+
patterns: [/\b4[\s-]?space\s+indent/i, /\bindent\s+with\s+4\s+spaces/i]
|
|
43493
43526
|
}
|
|
43494
43527
|
]
|
|
43495
43528
|
},
|
|
@@ -43651,10 +43684,11 @@ function hasUnbalancedBracketsOrQuotes(val) {
|
|
|
43651
43684
|
if (square < 0 || curly < 0) return true;
|
|
43652
43685
|
}
|
|
43653
43686
|
if (square !== 0 || curly !== 0) return true;
|
|
43654
|
-
const
|
|
43655
|
-
|
|
43656
|
-
|
|
43657
|
-
|
|
43687
|
+
const quoteChar = val[0];
|
|
43688
|
+
if (quoteChar === '"' || quoteChar === "'") {
|
|
43689
|
+
const count = (val.match(quoteChar === '"' ? /"/g : /'/g) || []).length;
|
|
43690
|
+
if (count % 2 !== 0) return true;
|
|
43691
|
+
}
|
|
43658
43692
|
return false;
|
|
43659
43693
|
}
|
|
43660
43694
|
async function checkFrontmatter(file2, _projectRoot) {
|
|
@@ -44039,7 +44073,9 @@ async function checkMcpSecurity(config2, _projectRoot) {
|
|
|
44039
44073
|
try {
|
|
44040
44074
|
if (!isEnvVarRef(server2.url)) {
|
|
44041
44075
|
const parsed = new URL(server2.url);
|
|
44042
|
-
|
|
44076
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
44077
|
+
const isLoopback = host === "localhost" || host === "::1" || host.startsWith("127.");
|
|
44078
|
+
if (parsed.protocol === "http:" && !isLoopback) {
|
|
44043
44079
|
issues.push({
|
|
44044
44080
|
severity: "warning",
|
|
44045
44081
|
check: "mcp-security",
|
|
@@ -44495,6 +44531,7 @@ function checkMissingFromClient(configs) {
|
|
|
44495
44531
|
function collectServerNameKeys(content, rootKey) {
|
|
44496
44532
|
const names = [];
|
|
44497
44533
|
let i2 = 0;
|
|
44534
|
+
let keyStart = -1;
|
|
44498
44535
|
let depth = 0;
|
|
44499
44536
|
let inString = false;
|
|
44500
44537
|
let escape2 = false;
|
|
@@ -44520,6 +44557,7 @@ function collectServerNameKeys(content, rootKey) {
|
|
|
44520
44557
|
inString = true;
|
|
44521
44558
|
collectingKey = true;
|
|
44522
44559
|
pendingKey = "";
|
|
44560
|
+
keyStart = i2;
|
|
44523
44561
|
} else {
|
|
44524
44562
|
inString = false;
|
|
44525
44563
|
let j3 = i2 + 1;
|
|
@@ -44528,7 +44566,11 @@ function collectServerNameKeys(content, rootKey) {
|
|
|
44528
44566
|
if (pendingKey === rootKey && rootKeyDepth === -1) {
|
|
44529
44567
|
rootKeyDepth = depth;
|
|
44530
44568
|
} else if (rootKeyDepth !== -1 && depth === rootKeyDepth + 1) {
|
|
44531
|
-
|
|
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 });
|
|
44532
44574
|
}
|
|
44533
44575
|
}
|
|
44534
44576
|
collectingKey = false;
|
|
@@ -44554,14 +44596,19 @@ function checkSingleFileIssues(configs) {
|
|
|
44554
44596
|
if (!config2.actualRootKey) continue;
|
|
44555
44597
|
const serverKeys = collectServerNameKeys(config2.content, config2.actualRootKey);
|
|
44556
44598
|
const counts = /* @__PURE__ */ new Map();
|
|
44557
|
-
|
|
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
|
+
}
|
|
44558
44605
|
for (const [name, count] of counts) {
|
|
44559
44606
|
if (count > 1) {
|
|
44560
44607
|
issues.push({
|
|
44561
44608
|
severity: "warning",
|
|
44562
44609
|
check: "mcp-consistency",
|
|
44563
44610
|
ruleId: "mcp-consistency/duplicate-server-name",
|
|
44564
|
-
line: 1,
|
|
44611
|
+
line: secondLines.get(name) ?? 1,
|
|
44565
44612
|
message: `Duplicate server name "${name}" in ${config2.relativePath} \u2014 only the last definition is used`
|
|
44566
44613
|
});
|
|
44567
44614
|
}
|
|
@@ -44720,6 +44767,17 @@ async function checkMcphApibase(config2, _projectRoot) {
|
|
|
44720
44767
|
});
|
|
44721
44768
|
return issues;
|
|
44722
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
|
+
}
|
|
44723
44781
|
if (parsed.protocol === "http:" && !isPrivateHost(parsed.hostname)) {
|
|
44724
44782
|
issues.push({
|
|
44725
44783
|
severity: "warning",
|
|
@@ -44864,13 +44922,13 @@ var init_gitignore = __esm({
|
|
|
44864
44922
|
// src/core/session-parser.ts
|
|
44865
44923
|
import { readFile } from "node:fs/promises";
|
|
44866
44924
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
44867
|
-
import { dirname as
|
|
44925
|
+
import { dirname as dirname3, resolve as resolve4 } from "node:path";
|
|
44868
44926
|
import { fileURLToPath } from "node:url";
|
|
44869
44927
|
function loadWebFirstSegments() {
|
|
44870
44928
|
if (typeof define_WEB_FIRST_SEGMENTS_default !== "undefined") {
|
|
44871
44929
|
return new Set(define_WEB_FIRST_SEGMENTS_default);
|
|
44872
44930
|
}
|
|
44873
|
-
const __dir =
|
|
44931
|
+
const __dir = dirname3(fileURLToPath(import.meta.url));
|
|
44874
44932
|
const fixturePath = resolve4(__dir, "../../tests/fixtures/web-first-segments.json");
|
|
44875
44933
|
const raw = JSON.parse(readFileSync4(fixturePath, "utf-8"));
|
|
44876
44934
|
const list = Array.isArray(raw) ? raw : raw.segments;
|
|
@@ -44913,6 +44971,12 @@ function extractPathsClassified(content) {
|
|
|
44913
44971
|
candidates.push(p2);
|
|
44914
44972
|
}
|
|
44915
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
|
+
}
|
|
44916
44980
|
const seen = /* @__PURE__ */ new Set();
|
|
44917
44981
|
const out = [];
|
|
44918
44982
|
for (const value of candidates) {
|
|
@@ -44965,7 +45029,7 @@ async function parseMemoryFile(filePath, projectDir) {
|
|
|
44965
45029
|
referencedPaths
|
|
44966
45030
|
};
|
|
44967
45031
|
}
|
|
44968
|
-
var WEB_FIRST_SEGMENTS, PATH_PATTERN2, BARE_FILE_PATH;
|
|
45032
|
+
var WEB_FIRST_SEGMENTS, PATH_PATTERN2, BARE_FILE_PATH, DRIVE_ABS_PATH;
|
|
44969
45033
|
var init_session_parser = __esm({
|
|
44970
45034
|
"src/core/session-parser.ts"() {
|
|
44971
45035
|
"use strict";
|
|
@@ -44974,6 +45038,7 @@ var init_session_parser = __esm({
|
|
|
44974
45038
|
WEB_FIRST_SEGMENTS = loadWebFirstSegments();
|
|
44975
45039
|
PATH_PATTERN2 = /(?:^|\s|['"`(])([.~/][^\s'"`),;:!?]+)/g;
|
|
44976
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;
|
|
44977
45042
|
}
|
|
44978
45043
|
});
|
|
44979
45044
|
|
|
@@ -44981,7 +45046,7 @@ var init_session_parser = __esm({
|
|
|
44981
45046
|
import { readdir, stat } from "node:fs/promises";
|
|
44982
45047
|
import { createReadStream } from "node:fs";
|
|
44983
45048
|
import { createInterface } from "node:readline";
|
|
44984
|
-
import { dirname as
|
|
45049
|
+
import { dirname as dirname4, join as join5, resolve as resolve5 } from "node:path";
|
|
44985
45050
|
import { existsSync } from "node:fs";
|
|
44986
45051
|
function detectProviders() {
|
|
44987
45052
|
if (!home) return [];
|
|
@@ -45076,7 +45141,7 @@ function detectAiderInSiblings(siblings) {
|
|
|
45076
45141
|
return null;
|
|
45077
45142
|
}
|
|
45078
45143
|
async function detectSiblings(projectRoot) {
|
|
45079
|
-
const parentDir =
|
|
45144
|
+
const parentDir = dirname4(resolve5(projectRoot));
|
|
45080
45145
|
let entries;
|
|
45081
45146
|
try {
|
|
45082
45147
|
entries = await readdir(parentDir);
|
|
@@ -45173,7 +45238,12 @@ var init_session_scanner = __esm({
|
|
|
45173
45238
|
},
|
|
45174
45239
|
{
|
|
45175
45240
|
provider: "goose",
|
|
45176
|
-
|
|
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")
|
|
45177
45247
|
},
|
|
45178
45248
|
{ provider: "continue", dir: join5(home, ".continue") },
|
|
45179
45249
|
{
|
|
@@ -45189,43 +45259,68 @@ import { resolve as resolve6, basename as basename3 } from "node:path";
|
|
|
45189
45259
|
function normalizePath2(p2) {
|
|
45190
45260
|
return resolve6(p2).replace(/\\/g, "/").toLowerCase();
|
|
45191
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
|
+
}
|
|
45192
45292
|
async function checkMissingSecret(ctx) {
|
|
45193
45293
|
const issues = [];
|
|
45194
45294
|
const secrets = [];
|
|
45195
45295
|
for (const entry of ctx.history) {
|
|
45196
|
-
const
|
|
45197
|
-
if (!
|
|
45296
|
+
const name = extractSecretName(entry.display);
|
|
45297
|
+
if (!name) continue;
|
|
45298
|
+
const repoMatch = entry.display.match(REPO_FLAG_PATTERN);
|
|
45198
45299
|
secrets.push({
|
|
45199
|
-
name
|
|
45200
|
-
repo:
|
|
45300
|
+
name,
|
|
45301
|
+
repo: repoMatch ? repoMatch[1] : void 0,
|
|
45201
45302
|
project: entry.project
|
|
45202
45303
|
});
|
|
45203
45304
|
}
|
|
45204
45305
|
if (secrets.length === 0) return issues;
|
|
45205
45306
|
const byName = /* @__PURE__ */ new Map();
|
|
45206
45307
|
for (const s of secrets) {
|
|
45207
|
-
if (!byName.has(s.name)) byName.set(s.name, /* @__PURE__ */ new Set());
|
|
45208
|
-
byName.get(s.name)
|
|
45209
|
-
|
|
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);
|
|
45210
45312
|
}
|
|
45211
45313
|
const currentNorm = normalizePath2(ctx.currentProject);
|
|
45212
45314
|
const currentBase = basename3(currentNorm);
|
|
45213
|
-
for (const [secretName, projects] of byName) {
|
|
45315
|
+
for (const [secretName, { projects, repos }] of byName) {
|
|
45214
45316
|
const normalizedProjects = [...projects].map(normalizePath2);
|
|
45215
|
-
const
|
|
45216
|
-
|
|
45217
|
-
const lastSegment = p2.split("/").pop() || "";
|
|
45218
|
-
return lastSegment === currentBase;
|
|
45219
|
-
});
|
|
45317
|
+
const repoSpecs = [...repos];
|
|
45318
|
+
const currentHas = normalizedProjects.some((p2) => p2 === currentNorm) || repoSpecs.some((r2) => repoBasename(r2) === currentBase);
|
|
45220
45319
|
if (currentHas) continue;
|
|
45221
45320
|
const siblingMatches = ctx.siblings.filter((sib) => {
|
|
45222
45321
|
const sibNorm = normalizePath2(sib.path);
|
|
45223
|
-
|
|
45224
|
-
return
|
|
45225
|
-
if (p2 === sibNorm) return true;
|
|
45226
|
-
const lastSegment = p2.split("/").pop() || "";
|
|
45227
|
-
return lastSegment === sibBase;
|
|
45228
|
-
});
|
|
45322
|
+
if (normalizedProjects.some((p2) => p2 === sibNorm)) return true;
|
|
45323
|
+
return repoSpecs.some((r2) => repoMatchesSibling(r2, sib));
|
|
45229
45324
|
});
|
|
45230
45325
|
if (siblingMatches.length >= 2) {
|
|
45231
45326
|
const sibNames = siblingMatches.map((s) => s.name).join(", ");
|
|
@@ -45242,19 +45337,20 @@ async function checkMissingSecret(ctx) {
|
|
|
45242
45337
|
}
|
|
45243
45338
|
return issues;
|
|
45244
45339
|
}
|
|
45245
|
-
var
|
|
45340
|
+
var SECRET_SET_PREFIX, REPO_FLAG_PATTERN, VALUE_FLAGS;
|
|
45246
45341
|
var init_missing_secret = __esm({
|
|
45247
45342
|
"src/core/checks/session/missing-secret.ts"() {
|
|
45248
45343
|
"use strict";
|
|
45249
45344
|
init_define_WEB_FIRST_SEGMENTS();
|
|
45250
|
-
|
|
45251
|
-
|
|
45345
|
+
SECRET_SET_PREFIX = /gh\s+secret\s+set\s+/;
|
|
45346
|
+
REPO_FLAG_PATTERN = /--repo\s+(\S+)/;
|
|
45347
|
+
VALUE_FLAGS = /* @__PURE__ */ new Set(["--repo", "-R", "-b", "--body", "--app", "--env"]);
|
|
45252
45348
|
}
|
|
45253
45349
|
});
|
|
45254
45350
|
|
|
45255
45351
|
// src/core/checks/session/diverged-file.ts
|
|
45256
45352
|
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
45257
|
-
import { join as join6 } from "node:path";
|
|
45353
|
+
import { join as join6, resolve as resolve7 } from "node:path";
|
|
45258
45354
|
import { existsSync as existsSync2 } from "node:fs";
|
|
45259
45355
|
async function loadLineSet(absPath) {
|
|
45260
45356
|
let mtimeMs;
|
|
@@ -45303,11 +45399,16 @@ async function checkDivergedFile(ctx) {
|
|
|
45303
45399
|
bothEmptyIsIdentical: true
|
|
45304
45400
|
});
|
|
45305
45401
|
if (overlap >= 0.2 && overlap < 0.9) {
|
|
45306
|
-
diverged.push({
|
|
45402
|
+
diverged.push({
|
|
45403
|
+
sibling: sib.name,
|
|
45404
|
+
overlap: Math.round(overlap * 100),
|
|
45405
|
+
path: resolve7(sibPath)
|
|
45406
|
+
});
|
|
45307
45407
|
}
|
|
45308
45408
|
}
|
|
45309
45409
|
if (diverged.length > 0) {
|
|
45310
45410
|
const details = diverged.map((d) => `${d.sibling} (${d.overlap}% overlap)`).join(", ");
|
|
45411
|
+
const sibPaths = diverged.map((d) => ` ${d.sibling}: ${d.path}`).join("\n");
|
|
45311
45412
|
issues.push({
|
|
45312
45413
|
severity: "warning",
|
|
45313
45414
|
check: "session-diverged-file",
|
|
@@ -45315,7 +45416,9 @@ async function checkDivergedFile(ctx) {
|
|
|
45315
45416
|
line: 0,
|
|
45316
45417
|
message: `${fileName} has diverged from sibling repos: ${details}`,
|
|
45317
45418
|
suggestion: `Compare with sibling versions to identify unintentional drift`,
|
|
45318
|
-
detail: `
|
|
45419
|
+
detail: `File: ${resolve7(currentPath)}
|
|
45420
|
+
Diverged siblings:
|
|
45421
|
+
${sibPaths}`
|
|
45319
45422
|
});
|
|
45320
45423
|
}
|
|
45321
45424
|
}
|
|
@@ -45402,15 +45505,15 @@ var init_missing_workflow = __esm({
|
|
|
45402
45505
|
|
|
45403
45506
|
// src/core/checks/session/stale-memory.ts
|
|
45404
45507
|
import { existsSync as existsSync4 } from "node:fs";
|
|
45405
|
-
import { resolve as
|
|
45508
|
+
import { resolve as resolve8, isAbsolute as isAbsolute3 } from "node:path";
|
|
45406
45509
|
function resolveRef2(ref, projectRoot) {
|
|
45407
45510
|
if (ref.startsWith("~/") || ref === "~") {
|
|
45408
45511
|
const home2 = process.env.HOME || process.env.USERPROFILE;
|
|
45409
45512
|
if (!home2) return null;
|
|
45410
|
-
return ref === "~" ? home2 :
|
|
45513
|
+
return ref === "~" ? home2 : resolve8(home2, ref.slice(2));
|
|
45411
45514
|
}
|
|
45412
|
-
if (
|
|
45413
|
-
return
|
|
45515
|
+
if (isAbsolute3(ref)) return ref;
|
|
45516
|
+
return resolve8(projectRoot, ref);
|
|
45414
45517
|
}
|
|
45415
45518
|
async function checkStaleMemory(ctx) {
|
|
45416
45519
|
const issues = [];
|
|
@@ -45434,6 +45537,7 @@ async function checkStaleMemory(ctx) {
|
|
|
45434
45537
|
ruleId: "session-stale-memory/stale-memory",
|
|
45435
45538
|
line: 0,
|
|
45436
45539
|
message: `Memory "${name}" references ${brokenPaths.length} path(s) that no longer exist: ${brokenPaths.join(", ")}`,
|
|
45540
|
+
affectedPaths: brokenPaths,
|
|
45437
45541
|
suggestion: `Update or remove the memory file: ${mem.filePath}`,
|
|
45438
45542
|
detail: `Memory files with broken path references may cause the AI agent to follow stale instructions`
|
|
45439
45543
|
});
|
|
@@ -45453,7 +45557,15 @@ var init_stale_memory = __esm({
|
|
|
45453
45557
|
async function checkDuplicateMemory(ctx) {
|
|
45454
45558
|
const issues = [];
|
|
45455
45559
|
const reported = /* @__PURE__ */ new Set();
|
|
45456
|
-
const lineSets = ctx.memories.
|
|
45560
|
+
const lineSets = new Array(ctx.memories.length);
|
|
45561
|
+
const getLineSet = (idx) => {
|
|
45562
|
+
let ls2 = lineSets[idx];
|
|
45563
|
+
if (ls2 === void 0) {
|
|
45564
|
+
ls2 = toLineSet(ctx.memories[idx].content, MIN_TOKEN_LEN2);
|
|
45565
|
+
lineSets[idx] = ls2;
|
|
45566
|
+
}
|
|
45567
|
+
return ls2;
|
|
45568
|
+
};
|
|
45457
45569
|
for (let i2 = 0; i2 < ctx.memories.length; i2++) {
|
|
45458
45570
|
for (let j3 = i2 + 1; j3 < ctx.memories.length; j3++) {
|
|
45459
45571
|
const a = ctx.memories[i2];
|
|
@@ -45463,7 +45575,7 @@ async function checkDuplicateMemory(ctx) {
|
|
|
45463
45575
|
const bIsCurrent = projectDirMatchesPath(b2.projectDir, ctx.currentProject);
|
|
45464
45576
|
if (!aIsCurrent && !bIsCurrent) continue;
|
|
45465
45577
|
if (a.content.length < 50 || b2.content.length < 50) continue;
|
|
45466
|
-
const overlap = jaccardSimilarityFromSets(
|
|
45578
|
+
const overlap = jaccardSimilarityFromSets(getLineSet(i2), getLineSet(j3));
|
|
45467
45579
|
if (overlap < 0.6) continue;
|
|
45468
45580
|
const pairKey = [a.filePath, b2.filePath].sort().join("::");
|
|
45469
45581
|
if (reported.has(pairKey)) continue;
|
|
@@ -45497,9 +45609,9 @@ var init_duplicate_memory = __esm({
|
|
|
45497
45609
|
});
|
|
45498
45610
|
|
|
45499
45611
|
// src/core/checks/session/loop-detection.ts
|
|
45500
|
-
import { resolve as
|
|
45612
|
+
import { resolve as resolve9 } from "node:path";
|
|
45501
45613
|
function normalizeProject(p2) {
|
|
45502
|
-
return
|
|
45614
|
+
return resolve9(p2).replace(/\\/g, "/");
|
|
45503
45615
|
}
|
|
45504
45616
|
function findConsecutiveRepeats(displays) {
|
|
45505
45617
|
const results = [];
|
|
@@ -45509,13 +45621,13 @@ function findConsecutiveRepeats(displays) {
|
|
|
45509
45621
|
while (j3 < displays.length && displays[j3] === displays[i2]) j3++;
|
|
45510
45622
|
const count = j3 - i2;
|
|
45511
45623
|
if (count >= CONSECUTIVE_THRESHOLD) {
|
|
45512
|
-
results.push({ command: displays[i2], count });
|
|
45624
|
+
results.push({ command: displays[i2], count, startIdx: i2, endIdx: j3 });
|
|
45513
45625
|
}
|
|
45514
45626
|
i2 = j3;
|
|
45515
45627
|
}
|
|
45516
45628
|
return results;
|
|
45517
45629
|
}
|
|
45518
|
-
function findCyclicPatterns(displays) {
|
|
45630
|
+
function findCyclicPatterns(displays, consecutiveSpans) {
|
|
45519
45631
|
const results = [];
|
|
45520
45632
|
for (let cycleLen = 2; cycleLen <= MAX_CYCLE_LENGTH; cycleLen++) {
|
|
45521
45633
|
for (let start = 0; start <= displays.length - cycleLen * CYCLE_REPEAT_THRESHOLD; start++) {
|
|
@@ -45533,8 +45645,13 @@ function findCyclicPatterns(displays) {
|
|
|
45533
45645
|
}
|
|
45534
45646
|
}
|
|
45535
45647
|
if (repeats >= CYCLE_REPEAT_THRESHOLD) {
|
|
45648
|
+
const cycleEnd = start + cycleLen * repeats;
|
|
45649
|
+
const overlapsConsecutive = consecutiveSpans.some(
|
|
45650
|
+
(s) => s.startIdx < cycleEnd && start < s.endIdx
|
|
45651
|
+
);
|
|
45652
|
+
if (overlapsConsecutive) continue;
|
|
45536
45653
|
const alreadyCovered = results.some(
|
|
45537
|
-
(r2) => r2.startIdx <= start && r2.startIdx + r2.cycle.length * r2.repeats >=
|
|
45654
|
+
(r2) => r2.startIdx <= start && r2.startIdx + r2.cycle.length * r2.repeats >= cycleEnd
|
|
45538
45655
|
);
|
|
45539
45656
|
if (!alreadyCovered) {
|
|
45540
45657
|
results.push({ cycle, repeats, startIdx: start });
|
|
@@ -45563,7 +45680,8 @@ async function checkLoopDetection(ctx) {
|
|
|
45563
45680
|
suggestion: "An agent may be looping on this command. Check history.jsonl for context on what went wrong"
|
|
45564
45681
|
});
|
|
45565
45682
|
}
|
|
45566
|
-
const
|
|
45683
|
+
const consecutiveSpans = repeats.map((r2) => ({ startIdx: r2.startIdx, endIdx: r2.endIdx }));
|
|
45684
|
+
const cycles = findCyclicPatterns(displays, consecutiveSpans);
|
|
45567
45685
|
for (const { cycle, repeats: reps } of cycles) {
|
|
45568
45686
|
const cycleStr = cycle.map((c3) => c3.length > 40 ? c3.slice(0, 37) + "..." : c3).join(" -> ");
|
|
45569
45687
|
issues.push({
|
|
@@ -45613,7 +45731,7 @@ async function checkMemoryIndexOverflow(ctx) {
|
|
|
45613
45731
|
issues.push({
|
|
45614
45732
|
severity: "warning",
|
|
45615
45733
|
check: "session-memory-index-overflow",
|
|
45616
|
-
ruleId: "session-memory-index-overflow/
|
|
45734
|
+
ruleId: "session-memory-index-overflow/line-overflow",
|
|
45617
45735
|
line: MAX_LINES + 1,
|
|
45618
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.`,
|
|
45619
45737
|
detail: `File: ${memoryFile}`,
|
|
@@ -45625,7 +45743,7 @@ async function checkMemoryIndexOverflow(ctx) {
|
|
|
45625
45743
|
issues.push({
|
|
45626
45744
|
severity: "warning",
|
|
45627
45745
|
check: "session-memory-index-overflow",
|
|
45628
|
-
ruleId: "session-memory-index-overflow/
|
|
45746
|
+
ruleId: "session-memory-index-overflow/byte-overflow",
|
|
45629
45747
|
line: 0,
|
|
45630
45748
|
message: `MEMORY.md is ${byteSize.toLocaleString()} bytes \u2014 only the first ${MAX_BYTES.toLocaleString()} bytes are loaded. ~${excess.toLocaleString()} bytes are effectively invisible.`,
|
|
45631
45749
|
detail: `File: ${memoryFile}`,
|
|
@@ -45668,9 +45786,12 @@ async function findReleaseWorkflows(projectRoot) {
|
|
|
45668
45786
|
}
|
|
45669
45787
|
try {
|
|
45670
45788
|
const content = stripBom(await readFile4(join9(workflowDir, f), "utf-8"));
|
|
45671
|
-
const nameMatch = content.match(
|
|
45672
|
-
if (nameMatch
|
|
45673
|
-
|
|
45789
|
+
const nameMatch = content.match(/^\s*name:\s*(.+?)\s*$/m);
|
|
45790
|
+
if (nameMatch) {
|
|
45791
|
+
const name = nameMatch[1].replace(/^(['"])(.*)\1$/, "$2");
|
|
45792
|
+
if (RELEASE_FILENAME_PATTERNS.some((p2) => p2.test(name))) {
|
|
45793
|
+
releaseWorkflows.push(f);
|
|
45794
|
+
}
|
|
45674
45795
|
}
|
|
45675
45796
|
} catch {
|
|
45676
45797
|
}
|
|
@@ -46524,6 +46645,7 @@ function extractPathsFromMessage(msg) {
|
|
|
46524
46645
|
function applyIgnoreRules(issues, rules) {
|
|
46525
46646
|
const compiled = compileRules(rules);
|
|
46526
46647
|
const kept = [];
|
|
46648
|
+
const keepMask = [];
|
|
46527
46649
|
let dropped = 0;
|
|
46528
46650
|
for (const issue2 of issues) {
|
|
46529
46651
|
let suppress = false;
|
|
@@ -46531,7 +46653,7 @@ function applyIgnoreRules(issues, rules) {
|
|
|
46531
46653
|
if (rule.check !== issue2.check) continue;
|
|
46532
46654
|
if (rule.pathPattern) {
|
|
46533
46655
|
if (issue2.check !== "session-stale-memory") continue;
|
|
46534
|
-
const paths = extractPathsFromMessage(issue2.message);
|
|
46656
|
+
const paths = issue2.affectedPaths ?? extractPathsFromMessage(issue2.message);
|
|
46535
46657
|
if (paths.length === 0) continue;
|
|
46536
46658
|
const allMatch = paths.every((p2) => rule.pathPattern.test(p2));
|
|
46537
46659
|
if (!allMatch) continue;
|
|
@@ -46543,12 +46665,15 @@ function applyIgnoreRules(issues, rules) {
|
|
|
46543
46665
|
}
|
|
46544
46666
|
if (suppress) {
|
|
46545
46667
|
dropped++;
|
|
46668
|
+
keepMask.push(false);
|
|
46546
46669
|
} else {
|
|
46547
46670
|
kept.push(issue2);
|
|
46671
|
+
keepMask.push(true);
|
|
46548
46672
|
}
|
|
46549
46673
|
}
|
|
46550
46674
|
return {
|
|
46551
46675
|
kept,
|
|
46676
|
+
keepMask,
|
|
46552
46677
|
dropped,
|
|
46553
46678
|
unusedRules: compiled.filter((r2) => !r2.fired).map(stripCompiled),
|
|
46554
46679
|
rulesMissingReason: rules.filter((r2) => !r2.reason)
|
|
@@ -46583,12 +46708,12 @@ var init_types3 = __esm({
|
|
|
46583
46708
|
|
|
46584
46709
|
// src/version.ts
|
|
46585
46710
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
46586
|
-
import { resolve as
|
|
46711
|
+
import { resolve as resolve12, dirname as dirname6 } from "node:path";
|
|
46587
46712
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
46588
46713
|
function loadVersion() {
|
|
46589
|
-
if (true) return "0.13.
|
|
46590
|
-
const __dir =
|
|
46591
|
-
const pkgPath =
|
|
46714
|
+
if (true) return "0.13.2";
|
|
46715
|
+
const __dir = dirname6(fileURLToPath2(import.meta.url));
|
|
46716
|
+
const pkgPath = resolve12(__dir, "../package.json");
|
|
46592
46717
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
46593
46718
|
return pkg.version;
|
|
46594
46719
|
}
|
|
@@ -46641,7 +46766,9 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
|
|
|
46641
46766
|
if (activeChecks.includes("tokens"))
|
|
46642
46767
|
checkPromises.push(checkTokens(file2, projectRoot, thresholds));
|
|
46643
46768
|
if (activeChecks.includes("tier-tokens"))
|
|
46644
|
-
checkPromises.push(
|
|
46769
|
+
checkPromises.push(
|
|
46770
|
+
checkTierTokens(file2, projectRoot, thresholds, Boolean(options.hooksGlobal))
|
|
46771
|
+
);
|
|
46645
46772
|
if (activeChecks.includes("redundancy"))
|
|
46646
46773
|
checkPromises.push(checkRedundancy(file2, projectRoot));
|
|
46647
46774
|
if (activeChecks.includes("frontmatter"))
|
|
@@ -46896,10 +47023,9 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
|
|
|
46896
47023
|
}
|
|
46897
47024
|
});
|
|
46898
47025
|
const applied = applyIgnoreRules(flat, options.ignoreRules);
|
|
46899
|
-
const keptSet = new Set(applied.kept);
|
|
46900
47026
|
const rebuilt = fileResults.map(() => []);
|
|
46901
47027
|
flat.forEach((issue2, i2) => {
|
|
46902
|
-
if (
|
|
47028
|
+
if (applied.keepMask[i2]) rebuilt[owners[i2]].push(issue2);
|
|
46903
47029
|
});
|
|
46904
47030
|
fileResults.forEach((fr, idx) => {
|
|
46905
47031
|
fr.issues = rebuilt[idx];
|
|
@@ -46913,9 +47039,13 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
|
|
|
46913
47039
|
let estimatedWaste = 0;
|
|
46914
47040
|
for (const fr of fileResults) {
|
|
46915
47041
|
for (const issue2 of fr.issues) {
|
|
46916
|
-
if (issue2.check
|
|
46917
|
-
|
|
46918
|
-
|
|
47042
|
+
if (issue2.check !== "redundancy") continue;
|
|
47043
|
+
if (typeof issue2.wastedTokens === "number") {
|
|
47044
|
+
estimatedWaste += issue2.wastedTokens;
|
|
47045
|
+
} else if (issue2.suggestion) {
|
|
47046
|
+
for (const m of issue2.suggestion.matchAll(/~(\d+)\s+tokens/g)) {
|
|
47047
|
+
estimatedWaste += parseInt(m[1], 10);
|
|
47048
|
+
}
|
|
46919
47049
|
}
|
|
46920
47050
|
}
|
|
46921
47051
|
}
|
|
@@ -47593,12 +47723,6 @@ function applyFixes(result, options = {}) {
|
|
|
47593
47723
|
} : console.log.bind(console);
|
|
47594
47724
|
const dryRun = options.dryRun ?? false;
|
|
47595
47725
|
const skipSymlinks = options.skipSymlinks ?? true;
|
|
47596
|
-
const symlinkFiles = /* @__PURE__ */ new Set();
|
|
47597
|
-
if (skipSymlinks) {
|
|
47598
|
-
for (const f of result.files) {
|
|
47599
|
-
if (f.isSymlink) symlinkFiles.add(f.path);
|
|
47600
|
-
}
|
|
47601
|
-
}
|
|
47602
47726
|
const fixesByFile = /* @__PURE__ */ new Map();
|
|
47603
47727
|
const dedupeKeys = /* @__PURE__ */ new Map();
|
|
47604
47728
|
const skippedSymlinks = /* @__PURE__ */ new Set();
|
|
@@ -48154,7 +48278,7 @@ var init_server3 = __esm({
|
|
|
48154
48278
|
checks: external_exports3.array(mcphCheckEnum).optional().describe("Specific mcph checks to run (default: all mcph-* checks)."),
|
|
48155
48279
|
includeGlobal: external_exports3.boolean().optional().describe("Also scan ~/.mcph.json (user-global config)."),
|
|
48156
48280
|
strictEnvToken: external_exports3.boolean().optional().describe(
|
|
48157
|
-
"Upgrade mcph-
|
|
48281
|
+
"Upgrade mcph-token-security/prefer-env-token from warning to error (env-var-only posture)."
|
|
48158
48282
|
)
|
|
48159
48283
|
},
|
|
48160
48284
|
{
|
|
@@ -54801,6 +54925,11 @@ function formatText(result, verbose = false) {
|
|
|
54801
54925
|
lines.push("Session audit scanned");
|
|
54802
54926
|
renderedAnySummary = true;
|
|
54803
54927
|
}
|
|
54928
|
+
if (groups.skill.length > 0) {
|
|
54929
|
+
if (renderedAnySummary) lines.push("");
|
|
54930
|
+
lines.push("Skill audit scanned");
|
|
54931
|
+
renderedAnySummary = true;
|
|
54932
|
+
}
|
|
54804
54933
|
if (!renderedAnySummary) {
|
|
54805
54934
|
lines.push(`Found ${result.files.length} file${result.files.length !== 1 ? "s" : ""}`);
|
|
54806
54935
|
}
|
|
@@ -54896,12 +55025,13 @@ function formatTokenReport(result) {
|
|
|
54896
55025
|
source_default.dim(" (counts use GPT-4 cl100k_base tokenizer \u2014 Claude counts may vary slightly)")
|
|
54897
55026
|
);
|
|
54898
55027
|
lines.push("");
|
|
54899
|
-
const
|
|
55028
|
+
const realFiles = result.files.filter((f) => !isSyntheticBucket(f.path));
|
|
55029
|
+
const maxPathLen = Math.max(...realFiles.map((f) => f.path.length), 4);
|
|
54900
55030
|
lines.push(
|
|
54901
55031
|
` ${source_default.dim("File".padEnd(maxPathLen))} ${source_default.dim("Tokens".padStart(8))} ${source_default.dim("Lines".padStart(6))}`
|
|
54902
55032
|
);
|
|
54903
55033
|
lines.push(` ${"-".repeat(maxPathLen)} ${"-".repeat(8)} ${"-".repeat(6)}`);
|
|
54904
|
-
for (const file2 of
|
|
55034
|
+
for (const file2 of realFiles) {
|
|
54905
55035
|
const tokenStr = file2.tokens.toLocaleString().padStart(8);
|
|
54906
55036
|
const lineStr = file2.lines.toString().padStart(6);
|
|
54907
55037
|
lines.push(` ${file2.path.padEnd(maxPathLen)} ${tokenStr} ${lineStr}`);
|
|
@@ -54922,7 +55052,7 @@ function formatTokenReport(result) {
|
|
|
54922
55052
|
return lines.join("\n");
|
|
54923
55053
|
}
|
|
54924
55054
|
function isSyntheticPath(p2) {
|
|
54925
|
-
return p2
|
|
55055
|
+
return isSyntheticBucket(p2) || p2.startsWith("~");
|
|
54926
55056
|
}
|
|
54927
55057
|
function formatSarif(result) {
|
|
54928
55058
|
const severityToLevel = {
|
|
@@ -55244,7 +55374,7 @@ async function runCli() {
|
|
|
55244
55374
|
new Option("--format <format>", "Output format: text, json, or sarif").choices(["text", "json", "sarif"]).default("text")
|
|
55245
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(
|
|
55246
55376
|
"--mcph-strict-env-token",
|
|
55247
|
-
"Upgrade mcph-
|
|
55377
|
+
"Upgrade mcph-token-security/prefer-env-token from warning to error (strict env-var-only posture)",
|
|
55248
55378
|
false
|
|
55249
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(
|
|
55250
55380
|
"--hooks-global",
|
|
@@ -55278,7 +55408,7 @@ async function runCli() {
|
|
|
55278
55408
|
if (result.files.length === 0) {
|
|
55279
55409
|
if (!options.quiet) {
|
|
55280
55410
|
if (options.format === "json") {
|
|
55281
|
-
console.log(
|
|
55411
|
+
console.log(formatJson(result));
|
|
55282
55412
|
} else if (options.format === "sarif") {
|
|
55283
55413
|
console.log(formatSarif(result));
|
|
55284
55414
|
} else {
|
|
@@ -55418,7 +55548,7 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
55418
55548
|
let liveOptions = options;
|
|
55419
55549
|
let liveActiveChecks = activeChecks;
|
|
55420
55550
|
try {
|
|
55421
|
-
const resolved = resolveSession(resolvedPath, opts);
|
|
55551
|
+
const resolved = resolveSession(resolvedPath, opts, true);
|
|
55422
55552
|
liveConfig = resolved.config;
|
|
55423
55553
|
liveOptions = resolved.options;
|
|
55424
55554
|
liveActiveChecks = resolved.activeChecks;
|
|
@@ -55540,9 +55670,9 @@ async function promptYesNo(question) {
|
|
|
55540
55670
|
rl.close();
|
|
55541
55671
|
}
|
|
55542
55672
|
}
|
|
55543
|
-
function resolveSession(resolvedPath, opts) {
|
|
55673
|
+
function resolveSession(resolvedPath, opts, throwOnConfigError = false) {
|
|
55544
55674
|
const configPath = opts.config ? path16.resolve(opts.config) : void 0;
|
|
55545
|
-
const config2 = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
|
|
55675
|
+
const config2 = configPath ? loadConfigFromPath(configPath, throwOnConfigError) : loadConfig(resolvedPath);
|
|
55546
55676
|
const mcpGlobal = opts.mcpGlobal || config2?.mcpGlobal || false;
|
|
55547
55677
|
const mcpOnly = opts.mcpOnly || config2?.mcpOnly || false;
|
|
55548
55678
|
const mcpFlag = opts.mcp || mcpGlobal || mcpOnly || config2?.mcp || false;
|
|
@@ -55619,10 +55749,11 @@ function resolveSession(resolvedPath, opts) {
|
|
|
55619
55749
|
const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
|
|
55620
55750
|
return { config: config2, options, activeChecks };
|
|
55621
55751
|
}
|
|
55622
|
-
function loadConfigFromPath(configPath) {
|
|
55752
|
+
function loadConfigFromPath(configPath, throwOnError = false) {
|
|
55623
55753
|
try {
|
|
55624
55754
|
return loadConfigFromExplicitPath(configPath);
|
|
55625
55755
|
} catch (err) {
|
|
55756
|
+
if (throwOnError) throw err;
|
|
55626
55757
|
const detail = err instanceof Error ? err.message : String(err);
|
|
55627
55758
|
console.error(`Error: ${detail}`);
|
|
55628
55759
|
process.exit(2);
|