@yawlabs/ctxlint 0.9.18 → 0.9.19
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 +2 -2
- package/dist/index.js +282 -159
- package/package.json +1 -1
package/.pre-commit-hooks.yaml
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Version-pinned so a checkout at `rev: vX.Y.Z` runs exactly that release
|
|
5
5
|
# of ctxlint — matches the pinning done by `ctxlint init`. release.sh keeps
|
|
6
6
|
# this in sync with package.json on each bump.
|
|
7
|
-
entry: npx @yawlabs/ctxlint@0.9.
|
|
7
|
+
entry: npx @yawlabs/ctxlint@0.9.19 --strict
|
|
8
8
|
language: node
|
|
9
9
|
always_run: true
|
|
10
10
|
pass_filenames: false
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ ctxlint is a linter purpose-built for this. It reads your context files, cross-r
|
|
|
28
28
|
- **Token-aware** — shows how much context window your files consume and flags redundant content
|
|
29
29
|
- **Every AI tool** — supports Claude Code, Cursor, Copilot, Windsurf, Gemini, Cline, Aider, and 14 more
|
|
30
30
|
- **Multiple outputs** — text, JSON, and SARIF (GitHub Code Scanning)
|
|
31
|
-
- **MCP server** —
|
|
31
|
+
- **MCP server** — 7 tools for IDE/agent integration with tool annotations for auto-approval
|
|
32
32
|
- **Watch mode** — `--watch` re-lints automatically when context files change
|
|
33
33
|
|
|
34
34
|
## Install
|
|
@@ -404,7 +404,7 @@ CLI flags override config file settings. Use `--config <path>` to load a config
|
|
|
404
404
|
|
|
405
405
|
## Use as MCP Server
|
|
406
406
|
|
|
407
|
-
ctxlint ships with an MCP server that exposes
|
|
407
|
+
ctxlint ships with an MCP server that exposes seven tools (`ctxlint_audit`, `ctxlint_mcp_audit`, `ctxlint_mcph_audit`, `ctxlint_session_audit`, `ctxlint_validate_path`, `ctxlint_token_report`, `ctxlint_fix`). All read-only tools declare annotations so MCP clients can skip confirmation dialogs.
|
|
408
408
|
|
|
409
409
|
Launch it with the `serve` subcommand (or the equivalent `--mcp-server` flag, kept for back-compat):
|
|
410
410
|
|
package/dist/index.js
CHANGED
|
@@ -41316,6 +41316,28 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
|
|
|
41316
41316
|
}
|
|
41317
41317
|
return result;
|
|
41318
41318
|
}
|
|
41319
|
+
function getNodeValue(node) {
|
|
41320
|
+
switch (node.type) {
|
|
41321
|
+
case "array":
|
|
41322
|
+
return node.children.map(getNodeValue);
|
|
41323
|
+
case "object":
|
|
41324
|
+
const obj = /* @__PURE__ */ Object.create(null);
|
|
41325
|
+
for (let prop of node.children) {
|
|
41326
|
+
const valueNode = prop.children[1];
|
|
41327
|
+
if (valueNode) {
|
|
41328
|
+
obj[prop.children[0].value] = getNodeValue(valueNode);
|
|
41329
|
+
}
|
|
41330
|
+
}
|
|
41331
|
+
return obj;
|
|
41332
|
+
case "null":
|
|
41333
|
+
case "string":
|
|
41334
|
+
case "number":
|
|
41335
|
+
case "boolean":
|
|
41336
|
+
return node.value;
|
|
41337
|
+
default:
|
|
41338
|
+
return void 0;
|
|
41339
|
+
}
|
|
41340
|
+
}
|
|
41319
41341
|
function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
41320
41342
|
const _scanner = createScanner(text, false);
|
|
41321
41343
|
const _jsonPath = [];
|
|
@@ -41664,7 +41686,7 @@ var init_edit = __esm({
|
|
|
41664
41686
|
});
|
|
41665
41687
|
|
|
41666
41688
|
// node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js
|
|
41667
|
-
var ScanError, SyntaxKind, parseTree2, ParseErrorCode;
|
|
41689
|
+
var ScanError, SyntaxKind, parseTree2, getNodeValue2, ParseErrorCode;
|
|
41668
41690
|
var init_main = __esm({
|
|
41669
41691
|
"node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js"() {
|
|
41670
41692
|
"use strict";
|
|
@@ -41701,6 +41723,7 @@ var init_main = __esm({
|
|
|
41701
41723
|
SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
|
|
41702
41724
|
})(SyntaxKind || (SyntaxKind = {}));
|
|
41703
41725
|
parseTree2 = parseTree;
|
|
41726
|
+
getNodeValue2 = getNodeValue;
|
|
41704
41727
|
(function(ParseErrorCode2) {
|
|
41705
41728
|
ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
|
|
41706
41729
|
ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
|
|
@@ -41754,11 +41777,7 @@ async function parseMchpConfig(file2, projectRoot, scopeOverride) {
|
|
|
41754
41777
|
result.parseErrors.push(".mcph.json must be a JSON object at the root");
|
|
41755
41778
|
return result;
|
|
41756
41779
|
}
|
|
41757
|
-
|
|
41758
|
-
result.raw = JSON.parse(stripComments2(content));
|
|
41759
|
-
} catch {
|
|
41760
|
-
return result;
|
|
41761
|
-
}
|
|
41780
|
+
result.raw = getNodeValue2(tree);
|
|
41762
41781
|
const rootProps = tree.children ?? [];
|
|
41763
41782
|
for (const prop of rootProps) {
|
|
41764
41783
|
if (prop.type !== "property" || !prop.children || prop.children.length < 2) continue;
|
|
@@ -41816,46 +41835,6 @@ function offsetToPosition(content, offset) {
|
|
|
41816
41835
|
}
|
|
41817
41836
|
return { line, column };
|
|
41818
41837
|
}
|
|
41819
|
-
function stripComments2(src) {
|
|
41820
|
-
let out = "";
|
|
41821
|
-
let i2 = 0;
|
|
41822
|
-
let inString = false;
|
|
41823
|
-
let escape2 = false;
|
|
41824
|
-
while (i2 < src.length) {
|
|
41825
|
-
const ch = src[i2];
|
|
41826
|
-
if (inString) {
|
|
41827
|
-
out += ch;
|
|
41828
|
-
if (escape2) {
|
|
41829
|
-
escape2 = false;
|
|
41830
|
-
} else if (ch === "\\") {
|
|
41831
|
-
escape2 = true;
|
|
41832
|
-
} else if (ch === '"') {
|
|
41833
|
-
inString = false;
|
|
41834
|
-
}
|
|
41835
|
-
i2++;
|
|
41836
|
-
continue;
|
|
41837
|
-
}
|
|
41838
|
-
if (ch === '"') {
|
|
41839
|
-
inString = true;
|
|
41840
|
-
out += ch;
|
|
41841
|
-
i2++;
|
|
41842
|
-
continue;
|
|
41843
|
-
}
|
|
41844
|
-
if (ch === "/" && src[i2 + 1] === "/") {
|
|
41845
|
-
while (i2 < src.length && src[i2] !== "\n") i2++;
|
|
41846
|
-
continue;
|
|
41847
|
-
}
|
|
41848
|
-
if (ch === "/" && src[i2 + 1] === "*") {
|
|
41849
|
-
i2 += 2;
|
|
41850
|
-
while (i2 < src.length && !(src[i2] === "*" && src[i2 + 1] === "/")) i2++;
|
|
41851
|
-
i2 += 2;
|
|
41852
|
-
continue;
|
|
41853
|
-
}
|
|
41854
|
-
out += ch;
|
|
41855
|
-
i2++;
|
|
41856
|
-
}
|
|
41857
|
-
return out;
|
|
41858
|
-
}
|
|
41859
41838
|
async function checkGitTracked2(filePath, projectRoot) {
|
|
41860
41839
|
try {
|
|
41861
41840
|
const git = getGit(projectRoot);
|
|
@@ -45242,7 +45221,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
45242
45221
|
import { resolve as resolve8, dirname as dirname3 } from "node:path";
|
|
45243
45222
|
import { fileURLToPath } from "node:url";
|
|
45244
45223
|
function loadVersion() {
|
|
45245
|
-
if (true) return "0.9.
|
|
45224
|
+
if (true) return "0.9.19";
|
|
45246
45225
|
const __dir = dirname3(fileURLToPath(import.meta.url));
|
|
45247
45226
|
const pkgPath = resolve8(__dir, "../package.json");
|
|
45248
45227
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
@@ -46190,6 +46169,8 @@ function applyFixes(result, options = {}) {
|
|
|
46190
46169
|
existing.push(fix);
|
|
46191
46170
|
fixesByLine.set(fix.line, existing);
|
|
46192
46171
|
}
|
|
46172
|
+
let perFileFixCount = 0;
|
|
46173
|
+
const perFileLogs = [];
|
|
46193
46174
|
for (const [lineNum, lineFixes] of fixesByLine) {
|
|
46194
46175
|
const lineIdx = lineNum - 1;
|
|
46195
46176
|
if (lineIdx < 0 || lineIdx >= lines.length) continue;
|
|
@@ -46197,9 +46178,9 @@ function applyFixes(result, options = {}) {
|
|
|
46197
46178
|
for (const fix of lineFixes) {
|
|
46198
46179
|
if (line.includes(fix.oldText)) {
|
|
46199
46180
|
line = line.replaceAll(fix.oldText, fix.newText);
|
|
46200
|
-
|
|
46181
|
+
perFileFixCount++;
|
|
46201
46182
|
const prefix = dryRun ? source_default.cyan(" Would fix") : source_default.green(" Fixed");
|
|
46202
|
-
|
|
46183
|
+
perFileLogs.push(
|
|
46203
46184
|
prefix + ` Line ${fix.line}: ${source_default.dim(fix.oldText)} ${source_default.dim("->")} ${fix.newText}`
|
|
46204
46185
|
);
|
|
46205
46186
|
}
|
|
@@ -46223,6 +46204,8 @@ function applyFixes(result, options = {}) {
|
|
|
46223
46204
|
fs7.writeFileSync(filePath, newContent, "utf-8");
|
|
46224
46205
|
}
|
|
46225
46206
|
filesModified.push(filePath);
|
|
46207
|
+
totalFixes += perFileFixCount;
|
|
46208
|
+
for (const m of perFileLogs) log(m);
|
|
46226
46209
|
}
|
|
46227
46210
|
}
|
|
46228
46211
|
return { totalFixes, filesModified };
|
|
@@ -46253,7 +46236,7 @@ function validateFilePathInput(rawPath) {
|
|
|
46253
46236
|
throw new Error("path contains disallowed characters");
|
|
46254
46237
|
}
|
|
46255
46238
|
}
|
|
46256
|
-
var contextCheckEnum, mcpCheckEnum, sessionCheckEnum, PATH_DISALLOWED, server, transport;
|
|
46239
|
+
var contextCheckEnum, mcpCheckEnum, mcphCheckEnum, sessionCheckEnum, PATH_DISALLOWED, server, transport;
|
|
46257
46240
|
var init_server3 = __esm({
|
|
46258
46241
|
async "src/mcp/server.ts"() {
|
|
46259
46242
|
"use strict";
|
|
@@ -46272,6 +46255,7 @@ var init_server3 = __esm({
|
|
|
46272
46255
|
init_version2();
|
|
46273
46256
|
contextCheckEnum = external_exports3.enum(ALL_CHECKS);
|
|
46274
46257
|
mcpCheckEnum = external_exports3.enum(ALL_MCP_CHECKS);
|
|
46258
|
+
mcphCheckEnum = external_exports3.enum(ALL_MCPH_CHECKS);
|
|
46275
46259
|
sessionCheckEnum = external_exports3.enum(ALL_SESSION_CHECKS);
|
|
46276
46260
|
PATH_DISALLOWED = /[\n\r\t;`|]|\$\(|\$\{/;
|
|
46277
46261
|
server = new McpServer({
|
|
@@ -46493,6 +46477,48 @@ var init_server3 = __esm({
|
|
|
46493
46477
|
}
|
|
46494
46478
|
}
|
|
46495
46479
|
);
|
|
46480
|
+
server.tool(
|
|
46481
|
+
"ctxlint_mcph_audit",
|
|
46482
|
+
"Lint .mcph.json (the @yawlabs/mcph CLI config) files. Checks for PAT (mcp_pat_*) leakage in git-tracked project-scope files, environment-variable posture, plaintext HTTP apiBase to public hosts, schema drift, allow/deny list conflicts and duplicates, and machine-local override files not covered by .gitignore. Distinct from ctxlint_mcp_audit, which lints client-side .mcp.json server lists.",
|
|
46483
|
+
{
|
|
46484
|
+
projectPath: external_exports3.string().optional().describe("Path to the project root. Defaults to current working directory."),
|
|
46485
|
+
checks: external_exports3.array(mcphCheckEnum).optional().describe("Specific mcph checks to run (default: all mcph-* checks)."),
|
|
46486
|
+
includeGlobal: external_exports3.boolean().optional().describe("Also scan ~/.mcph.json (user-global config)."),
|
|
46487
|
+
strictEnvToken: external_exports3.boolean().optional().describe(
|
|
46488
|
+
"Upgrade mcph-config/prefer-env-token from warning to error (env-var-only posture)."
|
|
46489
|
+
)
|
|
46490
|
+
},
|
|
46491
|
+
{
|
|
46492
|
+
readOnlyHint: true,
|
|
46493
|
+
destructiveHint: false,
|
|
46494
|
+
idempotentHint: true,
|
|
46495
|
+
openWorldHint: false
|
|
46496
|
+
},
|
|
46497
|
+
async ({ projectPath, checks, includeGlobal, strictEnvToken }) => {
|
|
46498
|
+
try {
|
|
46499
|
+
const root = validateProjectPath(projectPath);
|
|
46500
|
+
const activeChecks = checks?.length ? checks : ALL_MCPH_CHECKS;
|
|
46501
|
+
const result = await runAudit(root, activeChecks, {
|
|
46502
|
+
mcph: true,
|
|
46503
|
+
mcphOnly: true,
|
|
46504
|
+
mcphGlobal: includeGlobal || false,
|
|
46505
|
+
mcphStrictEnvToken: strictEnvToken || false
|
|
46506
|
+
});
|
|
46507
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
46508
|
+
} catch (err) {
|
|
46509
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46510
|
+
return {
|
|
46511
|
+
content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
|
|
46512
|
+
isError: true
|
|
46513
|
+
};
|
|
46514
|
+
} finally {
|
|
46515
|
+
freeEncoder();
|
|
46516
|
+
resetGit();
|
|
46517
|
+
resetPathsCache();
|
|
46518
|
+
resetPackageJsonCache();
|
|
46519
|
+
}
|
|
46520
|
+
}
|
|
46521
|
+
);
|
|
46496
46522
|
server.tool(
|
|
46497
46523
|
"ctxlint_session_audit",
|
|
46498
46524
|
"Audit AI agent session data for cross-project consistency. Checks for missing GitHub secrets, diverged config files, missing workflows, stale memory entries, and duplicate memories across sibling repositories.",
|
|
@@ -53008,6 +53034,24 @@ var init_ora = __esm({
|
|
|
53008
53034
|
});
|
|
53009
53035
|
|
|
53010
53036
|
// src/core/reporter.ts
|
|
53037
|
+
function classifyFile(f) {
|
|
53038
|
+
if (f.path === "(mcp)") return "mcp";
|
|
53039
|
+
if (f.path.includes("session audit")) return "session";
|
|
53040
|
+
for (const issue2 of f.issues) {
|
|
53041
|
+
if (issue2.check.startsWith("session-")) return "session";
|
|
53042
|
+
if (issue2.check.startsWith("mcph-")) return "mcph";
|
|
53043
|
+
if (issue2.check.startsWith("mcp-")) return "mcp";
|
|
53044
|
+
}
|
|
53045
|
+
const norm = f.path.replace(/\\/g, "/");
|
|
53046
|
+
if (norm.endsWith(".mcph.json") || norm.endsWith(".mcph.local.json")) return "mcph";
|
|
53047
|
+
if (norm === ".mcp.json" || norm.endsWith("/.mcp.json") || norm.endsWith("/mcp.json") || norm.includes("/mcpServers/") || norm.endsWith("/.claude.json") || norm.endsWith(".claude/settings.json") || norm.endsWith("claude_desktop_config.json")) {
|
|
53048
|
+
return "mcp";
|
|
53049
|
+
}
|
|
53050
|
+
return "context";
|
|
53051
|
+
}
|
|
53052
|
+
function isSyntheticBucket(p2) {
|
|
53053
|
+
return p2.startsWith("(") || p2.includes("session audit");
|
|
53054
|
+
}
|
|
53011
53055
|
function formatText(result, verbose = false) {
|
|
53012
53056
|
const lines = [];
|
|
53013
53057
|
lines.push("");
|
|
@@ -53015,30 +53059,45 @@ function formatText(result, verbose = false) {
|
|
|
53015
53059
|
lines.push("");
|
|
53016
53060
|
lines.push(`Scanning ${result.projectRoot}...`);
|
|
53017
53061
|
lines.push("");
|
|
53018
|
-
const
|
|
53019
|
-
|
|
53020
|
-
|
|
53062
|
+
const groups = {
|
|
53063
|
+
context: [],
|
|
53064
|
+
mcp: [],
|
|
53065
|
+
mcph: [],
|
|
53066
|
+
session: []
|
|
53067
|
+
};
|
|
53068
|
+
for (const f of result.files) {
|
|
53069
|
+
groups[classifyFile(f)].push(f);
|
|
53070
|
+
}
|
|
53021
53071
|
const totalTokens = result.summary.totalTokens;
|
|
53022
|
-
|
|
53072
|
+
let renderedAnySummary = false;
|
|
53073
|
+
const contextReal = groups.context.filter((f) => !isSyntheticBucket(f.path));
|
|
53074
|
+
if (contextReal.length > 0) {
|
|
53023
53075
|
lines.push(
|
|
53024
|
-
`Found ${
|
|
53076
|
+
`Found ${contextReal.length} context file${contextReal.length !== 1 ? "s" : ""} (${totalTokens.toLocaleString()} tokens total)`
|
|
53025
53077
|
);
|
|
53026
|
-
for (const file2 of
|
|
53078
|
+
for (const file2 of contextReal) {
|
|
53027
53079
|
let desc = ` ${file2.path} (${file2.tokens.toLocaleString()} tokens, ${file2.lines} lines)`;
|
|
53028
53080
|
if (file2.isSymlink && file2.symlinkTarget) {
|
|
53029
53081
|
desc = ` ${file2.path} ${source_default.dim(`-> ${file2.symlinkTarget} (symlink)`)}`;
|
|
53030
53082
|
}
|
|
53031
53083
|
lines.push(desc);
|
|
53032
53084
|
}
|
|
53085
|
+
renderedAnySummary = true;
|
|
53033
53086
|
}
|
|
53034
|
-
|
|
53035
|
-
|
|
53036
|
-
|
|
53037
|
-
|
|
53038
|
-
|
|
53039
|
-
}
|
|
53087
|
+
for (const g of ["mcp", "mcph"]) {
|
|
53088
|
+
const real = groups[g].filter((f) => !isSyntheticBucket(f.path));
|
|
53089
|
+
if (real.length === 0) continue;
|
|
53090
|
+
if (renderedAnySummary) lines.push("");
|
|
53091
|
+
lines.push(`Found ${real.length} ${GROUP_SUMMARY_NOUNS[g]}${real.length !== 1 ? "s" : ""}`);
|
|
53092
|
+
for (const file2 of real) lines.push(` ${file2.path}`);
|
|
53093
|
+
renderedAnySummary = true;
|
|
53094
|
+
}
|
|
53095
|
+
if (groups.session.length > 0) {
|
|
53096
|
+
if (renderedAnySummary) lines.push("");
|
|
53097
|
+
lines.push("Session audit scanned");
|
|
53098
|
+
renderedAnySummary = true;
|
|
53040
53099
|
}
|
|
53041
|
-
if (
|
|
53100
|
+
if (!renderedAnySummary) {
|
|
53042
53101
|
lines.push(`Found ${result.files.length} file${result.files.length !== 1 ? "s" : ""}`);
|
|
53043
53102
|
}
|
|
53044
53103
|
lines.push("");
|
|
@@ -53057,16 +53116,13 @@ function formatText(result, verbose = false) {
|
|
|
53057
53116
|
lines.push("");
|
|
53058
53117
|
}
|
|
53059
53118
|
};
|
|
53060
|
-
|
|
53061
|
-
|
|
53062
|
-
|
|
53063
|
-
|
|
53064
|
-
|
|
53065
|
-
|
|
53066
|
-
|
|
53067
|
-
if (mcpWithIssues.length > 0) {
|
|
53068
|
-
lines.push(source_default.bold("MCP Configs"));
|
|
53069
|
-
renderFileGroup(mcpFiles);
|
|
53119
|
+
const groupsToRender = GROUP_ORDER.filter(
|
|
53120
|
+
(g) => groups[g].some((f) => f.issues.length > 0 || verbose)
|
|
53121
|
+
);
|
|
53122
|
+
if (groupsToRender.length > 1) {
|
|
53123
|
+
for (const g of groupsToRender) {
|
|
53124
|
+
lines.push(source_default.bold(GROUP_LABELS[g]));
|
|
53125
|
+
renderFileGroup(groups[g]);
|
|
53070
53126
|
}
|
|
53071
53127
|
} else {
|
|
53072
53128
|
renderFileGroup(result.files);
|
|
@@ -53282,6 +53338,31 @@ function buildRuleDescriptors() {
|
|
|
53282
53338
|
shortDescription: { text: "Redundant MCP config entry" },
|
|
53283
53339
|
helpUri: "https://github.com/yawlabs/ctxlint#mcp-config-linting"
|
|
53284
53340
|
},
|
|
53341
|
+
{
|
|
53342
|
+
id: "ctxlint/mcph-token-security",
|
|
53343
|
+
shortDescription: { text: "mcp.hosting PAT leakage or env-var posture" },
|
|
53344
|
+
helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
|
|
53345
|
+
},
|
|
53346
|
+
{
|
|
53347
|
+
id: "ctxlint/mcph-apibase",
|
|
53348
|
+
shortDescription: { text: "mcph apiBase URL validation" },
|
|
53349
|
+
helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
|
|
53350
|
+
},
|
|
53351
|
+
{
|
|
53352
|
+
id: "ctxlint/mcph-schema-conformance",
|
|
53353
|
+
shortDescription: { text: "mcph config unknown field or stale schema version" },
|
|
53354
|
+
helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
|
|
53355
|
+
},
|
|
53356
|
+
{
|
|
53357
|
+
id: "ctxlint/mcph-lists",
|
|
53358
|
+
shortDescription: { text: "mcph allow/deny list conflict or duplicate entry" },
|
|
53359
|
+
helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
|
|
53360
|
+
},
|
|
53361
|
+
{
|
|
53362
|
+
id: "ctxlint/mcph-gitignore",
|
|
53363
|
+
shortDescription: { text: "mcph machine-local file not covered by .gitignore" },
|
|
53364
|
+
helpUri: "https://github.com/yawlabs/ctxlint#mcph-config-linting"
|
|
53365
|
+
},
|
|
53285
53366
|
{
|
|
53286
53367
|
id: "ctxlint/session-missing-secret",
|
|
53287
53368
|
shortDescription: { text: "GitHub secret set on sibling repos but missing here" },
|
|
@@ -53335,10 +53416,24 @@ function formatIssue(issue2) {
|
|
|
53335
53416
|
}
|
|
53336
53417
|
return line;
|
|
53337
53418
|
}
|
|
53419
|
+
var GROUP_ORDER, GROUP_LABELS, GROUP_SUMMARY_NOUNS;
|
|
53338
53420
|
var init_reporter = __esm({
|
|
53339
53421
|
"src/core/reporter.ts"() {
|
|
53340
53422
|
"use strict";
|
|
53341
53423
|
init_source();
|
|
53424
|
+
GROUP_ORDER = ["context", "mcp", "mcph", "session"];
|
|
53425
|
+
GROUP_LABELS = {
|
|
53426
|
+
context: "Context Files",
|
|
53427
|
+
mcp: "MCP Configs",
|
|
53428
|
+
mcph: "mcph Configs",
|
|
53429
|
+
session: "Session Audit"
|
|
53430
|
+
};
|
|
53431
|
+
GROUP_SUMMARY_NOUNS = {
|
|
53432
|
+
context: "context file",
|
|
53433
|
+
mcp: "MCP config",
|
|
53434
|
+
mcph: "mcph config",
|
|
53435
|
+
session: "session audit"
|
|
53436
|
+
};
|
|
53342
53437
|
}
|
|
53343
53438
|
});
|
|
53344
53439
|
|
|
@@ -53511,74 +53606,7 @@ async function runCli() {
|
|
|
53511
53606
|
false
|
|
53512
53607
|
).option("--session", "Run session audit checks (cross-project consistency)", false).option("--session-only", "Run only session checks, skip context and MCP checks", false).option("--watch", "Re-lint on context file changes", false).action(async (projectPath, opts) => {
|
|
53513
53608
|
const resolvedPath = path12.resolve(projectPath);
|
|
53514
|
-
const
|
|
53515
|
-
const config2 = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
|
|
53516
|
-
const mcpGlobal = opts.mcpGlobal || config2?.mcpGlobal || false;
|
|
53517
|
-
const mcpOnly = opts.mcpOnly || config2?.mcpOnly || false;
|
|
53518
|
-
const mcpFlag = opts.mcp || mcpGlobal || mcpOnly || config2?.mcp || false;
|
|
53519
|
-
const mcphGlobal = opts.mcphGlobal || config2?.mcphGlobal || false;
|
|
53520
|
-
const mcphOnly = opts.mcphOnly || config2?.mcphOnly || false;
|
|
53521
|
-
const mcphFlag = opts.mcph || mcphGlobal || mcphOnly || config2?.mcph || false;
|
|
53522
|
-
const mcphStrictEnvToken = opts.mcphStrictEnvToken || config2?.mcphStrictEnvToken || false;
|
|
53523
|
-
const sessionOnly = opts.sessionOnly || config2?.sessionOnly || false;
|
|
53524
|
-
const sessionFlag = opts.session || sessionOnly || config2?.session || false;
|
|
53525
|
-
let explicitChecks = opts.checks ? validateCheckNames(
|
|
53526
|
-
opts.checks.split(",").map((c3) => c3.trim()),
|
|
53527
|
-
"--checks"
|
|
53528
|
-
) : null;
|
|
53529
|
-
if (explicitChecks?.length === 0) explicitChecks = null;
|
|
53530
|
-
const hasMcpInChecks = explicitChecks?.some((c3) => c3.startsWith("mcp-")) || false;
|
|
53531
|
-
const hasMcphInChecks = explicitChecks?.some((c3) => c3.startsWith("mcph-")) || false;
|
|
53532
|
-
const hasSessionInChecks = explicitChecks?.some((c3) => c3.startsWith("session-")) || false;
|
|
53533
|
-
const effectiveMcp = mcpFlag || hasMcpInChecks;
|
|
53534
|
-
const effectiveMcph = mcphFlag || hasMcphInChecks;
|
|
53535
|
-
const effectiveSession = sessionFlag || sessionOnly || hasSessionInChecks;
|
|
53536
|
-
let checks;
|
|
53537
|
-
if (explicitChecks) {
|
|
53538
|
-
checks = explicitChecks;
|
|
53539
|
-
} else if (sessionOnly) {
|
|
53540
|
-
checks = ALL_SESSION_CHECKS;
|
|
53541
|
-
} else if (mcpOnly) {
|
|
53542
|
-
checks = ALL_MCP_CHECKS;
|
|
53543
|
-
} else if (mcphOnly) {
|
|
53544
|
-
checks = ALL_MCPH_CHECKS;
|
|
53545
|
-
} else {
|
|
53546
|
-
const base = config2?.checks || ALL_CHECKS;
|
|
53547
|
-
checks = [
|
|
53548
|
-
...base,
|
|
53549
|
-
...effectiveMcp ? ALL_MCP_CHECKS : [],
|
|
53550
|
-
...effectiveMcph ? ALL_MCPH_CHECKS : [],
|
|
53551
|
-
...effectiveSession ? ALL_SESSION_CHECKS : []
|
|
53552
|
-
];
|
|
53553
|
-
}
|
|
53554
|
-
const options = {
|
|
53555
|
-
projectPath: resolvedPath,
|
|
53556
|
-
checks,
|
|
53557
|
-
strict: opts.strict || config2?.strict || false,
|
|
53558
|
-
format: opts.format,
|
|
53559
|
-
verbose: opts.verbose,
|
|
53560
|
-
fix: opts.fix,
|
|
53561
|
-
ignore: opts.ignore ? validateCheckNames(
|
|
53562
|
-
opts.ignore.split(",").map((c3) => c3.trim()),
|
|
53563
|
-
"--ignore"
|
|
53564
|
-
) : config2?.ignore || [],
|
|
53565
|
-
tokensOnly: opts.tokens,
|
|
53566
|
-
quiet: opts.quiet,
|
|
53567
|
-
depth: Math.max(0, Math.min(parseInt(opts.depth, 10) || 2, 10)),
|
|
53568
|
-
mcp: effectiveMcp,
|
|
53569
|
-
mcpOnly,
|
|
53570
|
-
mcpGlobal,
|
|
53571
|
-
mcph: effectiveMcph,
|
|
53572
|
-
mcphOnly,
|
|
53573
|
-
mcphGlobal,
|
|
53574
|
-
mcphStrictEnvToken,
|
|
53575
|
-
session: effectiveSession,
|
|
53576
|
-
sessionOnly
|
|
53577
|
-
};
|
|
53578
|
-
if (config2?.tokenThresholds) {
|
|
53579
|
-
setTokenThresholds(config2.tokenThresholds);
|
|
53580
|
-
}
|
|
53581
|
-
const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
|
|
53609
|
+
const { config: config2, options, activeChecks } = resolveSession(resolvedPath, opts);
|
|
53582
53610
|
const spinner = options.format === "text" && !options.quiet ? ora("Scanning for context files...").start() : void 0;
|
|
53583
53611
|
try {
|
|
53584
53612
|
if (spinner) spinner.text = "Running checks...";
|
|
@@ -53706,7 +53734,16 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
53706
53734
|
path12.join(resolvedPath, "GEMINI.md"),
|
|
53707
53735
|
path12.join(resolvedPath, "replit.md"),
|
|
53708
53736
|
path12.join(resolvedPath, "package.json"),
|
|
53709
|
-
path12.join(resolvedPath, ".mcp.json")
|
|
53737
|
+
path12.join(resolvedPath, ".mcp.json"),
|
|
53738
|
+
// mcph configs — without these, edits to the .mcph.json the user is
|
|
53739
|
+
// actively iterating on don't trigger a re-lint.
|
|
53740
|
+
path12.join(resolvedPath, ".mcph.json"),
|
|
53741
|
+
path12.join(resolvedPath, ".mcph.local.json"),
|
|
53742
|
+
// ctxlint config — re-resolve on edit so threshold changes,
|
|
53743
|
+
// ignore-list edits, and check-list overrides take effect without
|
|
53744
|
+
// restarting the watcher.
|
|
53745
|
+
path12.join(resolvedPath, ".ctxlintrc"),
|
|
53746
|
+
path12.join(resolvedPath, ".ctxlintrc.json")
|
|
53710
53747
|
];
|
|
53711
53748
|
const watchDirs = [
|
|
53712
53749
|
path12.join(resolvedPath, ".claude"),
|
|
@@ -53727,30 +53764,46 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
53727
53764
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
53728
53765
|
debounceTimer = setTimeout(async () => {
|
|
53729
53766
|
if (process.stdout.isTTY) console.clear();
|
|
53767
|
+
let liveConfig = config2;
|
|
53768
|
+
let liveOptions = options;
|
|
53769
|
+
let liveActiveChecks = activeChecks;
|
|
53770
|
+
try {
|
|
53771
|
+
const resolved = resolveSession(resolvedPath, opts);
|
|
53772
|
+
liveConfig = resolved.config;
|
|
53773
|
+
liveOptions = resolved.options;
|
|
53774
|
+
liveActiveChecks = resolved.activeChecks;
|
|
53775
|
+
} catch (err) {
|
|
53776
|
+
console.error("Error reloading config:", err instanceof Error ? err.message : err);
|
|
53777
|
+
}
|
|
53730
53778
|
try {
|
|
53731
|
-
|
|
53732
|
-
|
|
53733
|
-
|
|
53734
|
-
|
|
53735
|
-
|
|
53736
|
-
|
|
53737
|
-
|
|
53738
|
-
|
|
53739
|
-
|
|
53740
|
-
|
|
53741
|
-
|
|
53742
|
-
|
|
53779
|
+
if (liveConfig?.tokenThresholds) {
|
|
53780
|
+
setTokenThresholds(liveConfig.tokenThresholds);
|
|
53781
|
+
} else {
|
|
53782
|
+
resetTokenThresholds();
|
|
53783
|
+
}
|
|
53784
|
+
const result = await runAudit(resolvedPath, liveActiveChecks, {
|
|
53785
|
+
depth: liveOptions.depth,
|
|
53786
|
+
extraPatterns: liveConfig?.contextFiles,
|
|
53787
|
+
mcp: liveOptions.mcp,
|
|
53788
|
+
mcpGlobal: liveOptions.mcpGlobal,
|
|
53789
|
+
mcpOnly: liveOptions.mcpOnly,
|
|
53790
|
+
mcph: liveOptions.mcph,
|
|
53791
|
+
mcphGlobal: liveOptions.mcphGlobal,
|
|
53792
|
+
mcphOnly: liveOptions.mcphOnly,
|
|
53793
|
+
mcphStrictEnvToken: liveOptions.mcphStrictEnvToken,
|
|
53794
|
+
session: liveOptions.session,
|
|
53795
|
+
sessionOnly: liveOptions.sessionOnly
|
|
53743
53796
|
});
|
|
53744
53797
|
if (result.files.length === 0) {
|
|
53745
53798
|
console.log("\nNo context files found.\n");
|
|
53746
|
-
} else if (
|
|
53799
|
+
} else if (liveOptions.tokensOnly) {
|
|
53747
53800
|
console.log(formatTokenReport(result));
|
|
53748
|
-
} else if (
|
|
53801
|
+
} else if (liveOptions.format === "json") {
|
|
53749
53802
|
console.log(formatJson(result));
|
|
53750
|
-
} else if (
|
|
53803
|
+
} else if (liveOptions.format === "sarif") {
|
|
53751
53804
|
console.log(formatSarif(result));
|
|
53752
53805
|
} else {
|
|
53753
|
-
console.log(formatText(result,
|
|
53806
|
+
console.log(formatText(result, liveOptions.verbose));
|
|
53754
53807
|
}
|
|
53755
53808
|
} catch (err) {
|
|
53756
53809
|
console.error("Error:", err instanceof Error ? err.message : err);
|
|
@@ -53760,7 +53813,6 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
|
|
|
53760
53813
|
resetPathsCache();
|
|
53761
53814
|
resetPackageJsonCache();
|
|
53762
53815
|
resetTokenThresholds();
|
|
53763
|
-
if (config2?.tokenThresholds) setTokenThresholds(config2.tokenThresholds);
|
|
53764
53816
|
}
|
|
53765
53817
|
console.log(chalk2.dim("\nWatching for changes... (Ctrl+C to stop)\n"));
|
|
53766
53818
|
}, 300);
|
|
@@ -53839,6 +53891,77 @@ async function promptYesNo(question) {
|
|
|
53839
53891
|
rl.close();
|
|
53840
53892
|
}
|
|
53841
53893
|
}
|
|
53894
|
+
function resolveSession(resolvedPath, opts) {
|
|
53895
|
+
const configPath = opts.config ? path12.resolve(opts.config) : void 0;
|
|
53896
|
+
const config2 = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
|
|
53897
|
+
const mcpGlobal = opts.mcpGlobal || config2?.mcpGlobal || false;
|
|
53898
|
+
const mcpOnly = opts.mcpOnly || config2?.mcpOnly || false;
|
|
53899
|
+
const mcpFlag = opts.mcp || mcpGlobal || mcpOnly || config2?.mcp || false;
|
|
53900
|
+
const mcphGlobal = opts.mcphGlobal || config2?.mcphGlobal || false;
|
|
53901
|
+
const mcphOnly = opts.mcphOnly || config2?.mcphOnly || false;
|
|
53902
|
+
const mcphFlag = opts.mcph || mcphGlobal || mcphOnly || config2?.mcph || false;
|
|
53903
|
+
const mcphStrictEnvToken = opts.mcphStrictEnvToken || config2?.mcphStrictEnvToken || false;
|
|
53904
|
+
const sessionOnly = opts.sessionOnly || config2?.sessionOnly || false;
|
|
53905
|
+
const sessionFlag = opts.session || sessionOnly || config2?.session || false;
|
|
53906
|
+
let explicitChecks = opts.checks ? validateCheckNames(
|
|
53907
|
+
opts.checks.split(",").map((c3) => c3.trim()),
|
|
53908
|
+
"--checks"
|
|
53909
|
+
) : null;
|
|
53910
|
+
if (explicitChecks?.length === 0) explicitChecks = null;
|
|
53911
|
+
const hasMcpInChecks = explicitChecks?.some((c3) => c3.startsWith("mcp-") && !c3.startsWith("mcph-")) || false;
|
|
53912
|
+
const hasMcphInChecks = explicitChecks?.some((c3) => c3.startsWith("mcph-")) || false;
|
|
53913
|
+
const hasSessionInChecks = explicitChecks?.some((c3) => c3.startsWith("session-")) || false;
|
|
53914
|
+
const effectiveMcp = mcpFlag || hasMcpInChecks;
|
|
53915
|
+
const effectiveMcph = mcphFlag || hasMcphInChecks;
|
|
53916
|
+
const effectiveSession = sessionFlag || sessionOnly || hasSessionInChecks;
|
|
53917
|
+
let checks;
|
|
53918
|
+
if (explicitChecks) {
|
|
53919
|
+
checks = explicitChecks;
|
|
53920
|
+
} else if (sessionOnly) {
|
|
53921
|
+
checks = ALL_SESSION_CHECKS;
|
|
53922
|
+
} else if (mcpOnly) {
|
|
53923
|
+
checks = ALL_MCP_CHECKS;
|
|
53924
|
+
} else if (mcphOnly) {
|
|
53925
|
+
checks = ALL_MCPH_CHECKS;
|
|
53926
|
+
} else {
|
|
53927
|
+
const base = config2?.checks || ALL_CHECKS;
|
|
53928
|
+
checks = [
|
|
53929
|
+
...base,
|
|
53930
|
+
...effectiveMcp ? ALL_MCP_CHECKS : [],
|
|
53931
|
+
...effectiveMcph ? ALL_MCPH_CHECKS : [],
|
|
53932
|
+
...effectiveSession ? ALL_SESSION_CHECKS : []
|
|
53933
|
+
];
|
|
53934
|
+
}
|
|
53935
|
+
const options = {
|
|
53936
|
+
projectPath: resolvedPath,
|
|
53937
|
+
checks,
|
|
53938
|
+
strict: opts.strict || config2?.strict || false,
|
|
53939
|
+
format: opts.format,
|
|
53940
|
+
verbose: opts.verbose,
|
|
53941
|
+
fix: opts.fix,
|
|
53942
|
+
ignore: opts.ignore ? validateCheckNames(
|
|
53943
|
+
opts.ignore.split(",").map((c3) => c3.trim()),
|
|
53944
|
+
"--ignore"
|
|
53945
|
+
) : config2?.ignore || [],
|
|
53946
|
+
tokensOnly: opts.tokens,
|
|
53947
|
+
quiet: opts.quiet,
|
|
53948
|
+
depth: Math.max(0, Math.min(parseInt(opts.depth, 10) || 2, 10)),
|
|
53949
|
+
mcp: effectiveMcp,
|
|
53950
|
+
mcpOnly,
|
|
53951
|
+
mcpGlobal,
|
|
53952
|
+
mcph: effectiveMcph,
|
|
53953
|
+
mcphOnly,
|
|
53954
|
+
mcphGlobal,
|
|
53955
|
+
mcphStrictEnvToken,
|
|
53956
|
+
session: effectiveSession,
|
|
53957
|
+
sessionOnly
|
|
53958
|
+
};
|
|
53959
|
+
if (config2?.tokenThresholds) {
|
|
53960
|
+
setTokenThresholds(config2.tokenThresholds);
|
|
53961
|
+
}
|
|
53962
|
+
const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
|
|
53963
|
+
return { config: config2, options, activeChecks };
|
|
53964
|
+
}
|
|
53842
53965
|
function loadConfigFromPath(configPath) {
|
|
53843
53966
|
try {
|
|
53844
53967
|
return loadConfigFromExplicitPath(configPath);
|