@lousy-agents/mcp 5.11.3 → 5.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mcp-server.js +182 -8
  2. package/package.json +1 -1
@@ -57950,6 +57950,24 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
57950
57950
  "Validation Suite",
57951
57951
  "Commands"
57952
57952
  ];
57953
+ /**
57954
+ * Descriptions for each default structural heading pattern explaining why it is recommended.
57955
+ * Keyed by Record<StructuralHeadingPattern, string> to ensure compile-time coverage — every
57956
+ * entry in DEFAULT_STRUCTURAL_HEADING_PATTERNS must have a description here.
57957
+ */ const HEADING_PATTERN_DESCRIPTION_RECORD = {
57958
+ // biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
57959
+ Validation: "Agents need this section to understand how to validate that their changes meet quality standards.",
57960
+ // biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
57961
+ Verification: "Agents need this section to understand how to verify their implementation is correct.",
57962
+ "Feedback Loop": "Agents need this section to understand the iterative improvement process to follow.",
57963
+ // biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
57964
+ Mandatory: "Agents need this section to understand which steps are required and cannot be skipped.",
57965
+ "Before Commit": "Agents need this section to know what checks to run before committing changes.",
57966
+ "Validation Suite": "Agents need this section to know which validation commands to run against the codebase.",
57967
+ // biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
57968
+ Commands: "Agents need this section to know which commands and tools are available in the project."
57969
+ };
57970
+ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESCRIPTION_RECORD));
57953
57971
  /** Conditional keywords indicating error handling near code blocks */ const CONDITIONAL_KEYWORDS = [
57954
57972
  "if",
57955
57973
  "fail",
@@ -57969,6 +57987,24 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
57969
57987
  * Use case for analyzing instruction quality across a repository.
57970
57988
  * Orchestrates discovery, AST parsing, and scoring of feedback loop documentation.
57971
57989
  */
57990
+
57991
+ /** Maximum number of raw heading pattern entries accepted before deduplication. */ const MAX_RAW_HEADING_PATTERNS = 1000;
57992
+ /** Maximum number of heading patterns accepted in a single execute() call. */ const MAX_HEADING_PATTERNS = 50;
57993
+ /** Maximum character length for a single heading pattern. */ const MAX_PATTERN_LENGTH = 200;
57994
+ /**
57995
+ * Lowercase-keyed lookup built once from HEADING_PATTERN_DESCRIPTIONS.
57996
+ * Enables description lookup regardless of the casing callers supply for patterns.
57997
+ */ const HEADING_DESCRIPTIONS_BY_LOWER = new Map(Array.from(HEADING_PATTERN_DESCRIPTIONS, ([k, v])=>[
57998
+ k.toLowerCase(),
57999
+ v
58000
+ ]));
58001
+ /**
58002
+ * Input for the analyze instruction quality use case.
58003
+ */ const AnalyzeInstructionQualityInputSchema = schemas_object({
58004
+ targetDir: schemas_string().min(1, "Target directory is required"),
58005
+ headingPatterns: schemas_array(schemas_string().max(MAX_PATTERN_LENGTH * 2)).max(MAX_RAW_HEADING_PATTERNS).optional(),
58006
+ proximityWindow: schemas_number().int().positive().optional()
58007
+ });
57972
58008
  /**
57973
58009
  * Use case for analyzing instruction quality.
57974
58010
  */ class AnalyzeInstructionQualityUseCase {
@@ -57980,16 +58016,107 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
57980
58016
  this.astGateway = astGateway;
57981
58017
  this.commandsGateway = commandsGateway;
57982
58018
  }
58019
+ /**
58020
+ * Returns true if the given heading-pattern string contains any characters
58021
+ * that could cause terminal injection or garbled output when the pattern
58022
+ * is embedded in a diagnostic message.
58023
+ *
58024
+ * Rejects: ASCII C0 (0x00–0x1F), DEL (0x7F), C1 (0x80–0x9F), **lone**
58025
+ * Unicode surrogates (U+D800–U+DFFF — but valid surrogate pairs like emoji
58026
+ * are accepted), line/paragraph separators (U+2028–U+2029), and bidi
58027
+ * override characters (U+202A–U+202E, U+2066–U+2069).
58028
+ */ static patternHasControlCharacters(value) {
58029
+ for(let i = 0; i < value.length; i++){
58030
+ const code = value.charCodeAt(i);
58031
+ if (code <= 0x1f) return true;
58032
+ if (code === 0x7f) return true;
58033
+ if (code >= 0x80 && code <= 0x9f) return true;
58034
+ // Detect lone surrogates only; valid surrogate pairs (e.g., emoji)
58035
+ // should be accepted. A high surrogate (0xD800–0xDBFF) is lone if
58036
+ // it is not immediately followed by a low surrogate (0xDC00–0xDFFF).
58037
+ if (code >= 0xd800 && code <= 0xdbff) {
58038
+ const next = value.charCodeAt(i + 1);
58039
+ if (next >= 0xdc00 && next <= 0xdfff) {
58040
+ i++; // valid pair — skip the low surrogate
58041
+ } else {
58042
+ return true; // lone high surrogate
58043
+ }
58044
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
58045
+ return true; // lone low surrogate
58046
+ }
58047
+ if (code === 0x2028 || code === 0x2029) return true;
58048
+ if (code >= 0x202a && code <= 0x202e) return true;
58049
+ if (code >= 0x2066 && code <= 0x2069) return true;
58050
+ }
58051
+ return false;
58052
+ }
58053
+ /**
58054
+ * Serializes a heading pattern for safe inclusion in error messages.
58055
+ * Produces a quoted, unambiguous representation by:
58056
+ * - Escaping `"` as `\"` and `\` as `\\` within printable ASCII (0x20–0x7E)
58057
+ * - Escaping every code unit outside that range to `\uXXXX`
58058
+ *
58059
+ * The \uXXXX escaping prevents terminal injection via bidi override
58060
+ * characters and other non-printable code points that JSON.stringify does
58061
+ * not escape.
58062
+ */ static serializePatternForError(value) {
58063
+ let result = '"';
58064
+ for(let i = 0; i < value.length; i++){
58065
+ const code = value.charCodeAt(i);
58066
+ if (code >= 0x20 && code <= 0x7e) {
58067
+ if (code === 0x22) {
58068
+ result += '\\"';
58069
+ } else if (code === 0x5c) {
58070
+ result += "\\\\";
58071
+ } else {
58072
+ result += value[i];
58073
+ }
58074
+ } else {
58075
+ result += `\\u${code.toString(16).padStart(4, "0")}`;
58076
+ }
58077
+ }
58078
+ return `${result}"`;
58079
+ }
57983
58080
  async execute(input) {
57984
- if (!input.targetDir) {
57985
- throw new Error("Target directory is required");
58081
+ let parsed;
58082
+ try {
58083
+ parsed = AnalyzeInstructionQualityInputSchema.parse(input);
58084
+ } catch (error) {
58085
+ if (error instanceof ZodError) {
58086
+ throw new Error(error.issues[0]?.message ?? "Validation failed: unable to parse error details");
58087
+ }
58088
+ throw error;
57986
58089
  }
57987
- const headingPatterns = input.headingPatterns ?? [
58090
+ const seenLower = new Set();
58091
+ const headingPatterns = [];
58092
+ for (const raw of parsed.headingPatterns ?? [
57988
58093
  ...DEFAULT_STRUCTURAL_HEADING_PATTERNS
57989
- ];
57990
- const proximityWindow = input.proximityWindow ?? 3;
57991
- const discoveredFiles = await this.discoveryGateway.discoverInstructionFiles(input.targetDir);
57992
- const mandatoryCommands = await this.commandsGateway.getMandatoryCommands(input.targetDir);
58094
+ ]){
58095
+ // Validate BEFORE trim() U+2028/U+2029 are JavaScript whitespace
58096
+ // and would be silently stripped by trim(), bypassing the check.
58097
+ // Filter before transform.
58098
+ if (AnalyzeInstructionQualityUseCase.patternHasControlCharacters(raw)) {
58099
+ throw new Error(`headingPatterns must not contain control characters, bidi override characters, or lone surrogate code points: ${AnalyzeInstructionQualityUseCase.serializePatternForError(raw)}`);
58100
+ }
58101
+ const trimmed = raw.trim();
58102
+ if (trimmed.length === 0) {
58103
+ throw new Error("headingPatterns must not contain empty or whitespace-only entries");
58104
+ }
58105
+ if (trimmed.length > MAX_PATTERN_LENGTH) {
58106
+ throw new Error(`headingPatterns entries must not exceed ${MAX_PATTERN_LENGTH} characters`);
58107
+ }
58108
+ const lower = trimmed.toLowerCase();
58109
+ if (!seenLower.has(lower)) {
58110
+ seenLower.add(lower);
58111
+ if (seenLower.size > MAX_HEADING_PATTERNS) {
58112
+ throw new Error(`headingPatterns must contain at most ${MAX_HEADING_PATTERNS} unique entries (case-insensitive)`);
58113
+ }
58114
+ headingPatterns.push(trimmed);
58115
+ }
58116
+ }
58117
+ const proximityWindow = parsed.proximityWindow ?? 3;
58118
+ const discoveredFiles = await this.discoveryGateway.discoverInstructionFiles(parsed.targetDir);
58119
+ const mandatoryCommands = await this.commandsGateway.getMandatoryCommands(parsed.targetDir);
57993
58120
  if (discoveredFiles.length === 0) {
57994
58121
  return {
57995
58122
  result: {
@@ -58022,7 +58149,7 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
58022
58149
  }
58023
58150
  }
58024
58151
  // Sort parsing errors for deterministic output
58025
- parsingErrors.sort((a, b)=>a.filePath.localeCompare(b.filePath));
58152
+ parsingErrors.sort((a, b)=>a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0);
58026
58153
  // Score each mandatory command across all files
58027
58154
  const commandScores = [];
58028
58155
  const diagnostics = [];
@@ -58037,6 +58164,16 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
58037
58164
  target: "instruction"
58038
58165
  });
58039
58166
  }
58167
+ // Check each successfully parsed file for missing structural headings
58168
+ const sortedFilePaths = Array.from(fileStructures.keys()).sort((a, b)=>a < b ? -1 : a > b ? 1 : 0);
58169
+ for (const filePath of sortedFilePaths){
58170
+ const structure = fileStructures.get(filePath);
58171
+ if (!structure) {
58172
+ continue;
58173
+ }
58174
+ const missingDiagnostics = this.checkMissingHeadings(filePath, structure, headingPatterns);
58175
+ diagnostics.push(...missingDiagnostics);
58176
+ }
58040
58177
  for (const command of mandatoryCommands){
58041
58178
  const bestAnalysis = this.findBestScore(command, discoveredFiles, fileStructures, headingPatterns, proximityWindow, diagnostics);
58042
58179
  if (bestAnalysis) {
@@ -58278,6 +58415,43 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
58278
58415
  }
58279
58416
  return false;
58280
58417
  }
58418
+ /**
58419
+ * Emits a warning diagnostic for each heading pattern that is absent from the file.
58420
+ * Headings are matched case-insensitively using substring inclusion, matching the
58421
+ * same logic used elsewhere in the use case.
58422
+ */ checkMissingHeadings(filePath, structure, headingPatterns) {
58423
+ // Precondition: `headingPatterns` must already be deduplicated
58424
+ // (case-insensitively) and validated. This invariant is enforced by
58425
+ // `execute()` before calling this method.
58426
+ const diagnostics = [];
58427
+ const fileHeadingTexts = structure.headings.map((h)=>h.text.toLowerCase());
58428
+ for (const pattern of headingPatterns){
58429
+ const patternLower = pattern.toLowerCase();
58430
+ const hasHeading = fileHeadingTexts.some((text)=>{
58431
+ if (!text.includes(patternLower)) {
58432
+ return false;
58433
+ }
58434
+ // Don't let a heading satisfy a shorter pattern when it also satisfies
58435
+ // a longer, more-specific pattern that starts with the shorter one.
58436
+ // E.g., a "Validation Suite" heading satisfies "Validation Suite" but
58437
+ // should NOT also satisfy "Validation".
58438
+ const supersededByMoreSpecific = headingPatterns.some((other)=>other !== pattern && other.toLowerCase().startsWith(patternLower) && text.includes(other.toLowerCase()));
58439
+ return !supersededByMoreSpecific;
58440
+ });
58441
+ if (!hasHeading) {
58442
+ const description = HEADING_DESCRIPTIONS_BY_LOWER.get(patternLower) ?? "This heading helps guide coding agents through structured workflows.";
58443
+ diagnostics.push({
58444
+ filePath,
58445
+ line: 1,
58446
+ severity: "warning",
58447
+ message: `Missing '${pattern}' heading section. ${description}`,
58448
+ ruleId: "instruction/missing-structural-heading",
58449
+ target: "instruction"
58450
+ });
58451
+ }
58452
+ }
58453
+ return diagnostics;
58454
+ }
58281
58455
  generateSuggestions(commandScores) {
58282
58456
  const suggestions = [];
58283
58457
  const lowStructural = commandScores.filter((s)=>s.structuralContext === 0 && s.bestSourceFile !== "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/mcp",
3
- "version": "5.11.3",
3
+ "version": "5.12.0",
4
4
  "description": "MCP server for lousy-agents - provides AI coding assistant tools via the Model Context Protocol",
5
5
  "type": "module",
6
6
  "repository": {