pr-checkmate 4.0.0 → 4.1.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.
- package/README.md +6 -0
- package/cspell.json +1 -0
- package/dist/cjs/agent/hook.d.ts +2 -1
- package/dist/cjs/agent/setup.d.ts +4 -3
- package/dist/cjs/core/checks/git/diff-smell.js +3 -1
- package/dist/cjs/core/checks/languages/lint.js +27 -0
- package/dist/cjs/core/checks/languages/locate.d.ts +7 -0
- package/dist/cjs/core/checks/languages/locate.js +68 -0
- package/dist/cjs/core/checks/languages/prettier.js +15 -2
- package/dist/cjs/core/checks/languages/typecheck.js +4 -1
- package/dist/cjs/core/checks/quality/yaml-lint.d.ts +15 -0
- package/dist/cjs/core/checks/quality/yaml-lint.js +33 -1
- package/dist/cjs/core/sarif.d.ts +3 -1
- package/dist/cjs/mcp/dependency.d.ts +2 -1
- package/dist/cjs/mcp/dependency.js +28 -22
- package/dist/cjs/mcp/protocol.d.ts +3 -9
- package/dist/cjs/mcp/review.d.ts +3 -2
- package/dist/cjs/mcp/review.js +1 -1
- package/dist/cjs/mcp/tools.js +43 -58
- package/dist/esm/agent/hook.d.ts +2 -1
- package/dist/esm/agent/setup.d.ts +4 -3
- package/dist/esm/core/checks/git/diff-smell.js +3 -1
- package/dist/esm/core/checks/languages/lint.js +28 -1
- package/dist/esm/core/checks/languages/locate.d.ts +7 -0
- package/dist/esm/core/checks/languages/locate.js +60 -0
- package/dist/esm/core/checks/languages/prettier.js +15 -2
- package/dist/esm/core/checks/languages/typecheck.js +4 -1
- package/dist/esm/core/checks/quality/yaml-lint.d.ts +15 -0
- package/dist/esm/core/checks/quality/yaml-lint.js +33 -2
- package/dist/esm/core/sarif.d.ts +3 -1
- package/dist/esm/mcp/dependency.d.ts +2 -1
- package/dist/esm/mcp/dependency.js +28 -22
- package/dist/esm/mcp/protocol.d.ts +3 -9
- package/dist/esm/mcp/review.d.ts +3 -2
- package/dist/esm/mcp/review.js +1 -1
- package/dist/esm/mcp/tools.js +43 -58
- package/package.json +2 -2
- package/src/config/cspell.json +1 -0
package/README.md
CHANGED
|
@@ -78,6 +78,12 @@ claude mcp add --scope user pr-checkmate -- pr-checkmate mcp # once, for every
|
|
|
78
78
|
npx pr-checkmate agent-setup # per repository, below
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
Then check it: `claude mcp list` shows `pr-checkmate ✔ Connected`; sessions that were already open
|
|
82
|
+
need a restart. `claude` comes from the Claude Code CLI (an editor extension keeps its own copy off
|
|
83
|
+
your `PATH`). To stop Claude Code asking before every call, allow `mcp__pr-checkmate` in
|
|
84
|
+
`permissions.allow`
|
|
85
|
+
([details](https://github.com/Mybono/pr_checkmate_public/blob/main/documentation/ai-agents.md#after-installing)).
|
|
86
|
+
|
|
81
87
|
| Tool | The agent calls it |
|
|
82
88
|
|---|---|
|
|
83
89
|
| `get_rules` | before writing — what blocks a PR, what only warns, your custom rules and banned imports |
|
package/cspell.json
CHANGED
package/dist/cjs/agent/hook.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare const HOOK_COMMAND = "pr-checkmate hook";
|
|
|
10
10
|
* @returns the scope to review, or `undefined` when the line commits nothing.
|
|
11
11
|
*/
|
|
12
12
|
export declare function commitScopeFor(command: string): ReviewScope | undefined;
|
|
13
|
-
|
|
13
|
+
interface HookVerdict {
|
|
14
14
|
/** 2 blocks the tool call, 0 lets it through. */
|
|
15
15
|
exitCode: 0 | 2;
|
|
16
16
|
/** Shown to the model when blocked. */
|
|
@@ -35,3 +35,4 @@ export declare const EDIT_CHECKS: readonly string[];
|
|
|
35
35
|
* @param fallbackCwd Used when the payload carries no directory.
|
|
36
36
|
*/
|
|
37
37
|
export declare function evaluateHook(stdin: string, fallbackCwd: string): Promise<HookVerdict>;
|
|
38
|
+
export {};
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
export declare function isOnPath(name: string, env?: NodeJS.ProcessEnv): boolean;
|
|
3
3
|
export declare const BLOCK_START = "<!-- pr-checkmate:agent-rules:start -->";
|
|
4
4
|
export declare const BLOCK_END = "<!-- pr-checkmate:agent-rules:end -->";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type SetupStatus = 'created' | 'updated' | 'unchanged' | 'skipped';
|
|
6
|
+
interface SetupAction {
|
|
7
7
|
file: string;
|
|
8
8
|
status: SetupStatus;
|
|
9
9
|
detail: string;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
interface SetupOptions {
|
|
12
12
|
/** Install the commit-gate hook. Default true. */
|
|
13
13
|
hook?: boolean;
|
|
14
14
|
/** Whether the CLI is on PATH. Detected when omitted; injectable for tests. */
|
|
@@ -18,3 +18,4 @@ export interface SetupOptions {
|
|
|
18
18
|
export declare function setupAgent(cwd: string, opts?: SetupOptions): SetupAction[];
|
|
19
19
|
/** Print what changed and the one step the repository cannot do for the user. */
|
|
20
20
|
export declare function reportSetup(actions: SetupAction[]): void;
|
|
21
|
+
export {};
|
|
@@ -22,7 +22,9 @@ exports.CODE_SMELLS = [
|
|
|
22
22
|
view: 'code',
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
|
|
25
|
+
// A statement, not a mention: `debugger` ends the line or follows a `;`, `{`, `}` or `)`.
|
|
26
|
+
// A regex literal (`/debugger/`) or an object key (`{ debugger: 1 }`) is not one.
|
|
27
|
+
pattern: /^\+(?!\+)(?:.*[;{})]|\s*)\s*debugger\b\s*;?\s*$/,
|
|
26
28
|
label: 'debugger statement',
|
|
27
29
|
severity: 'warn',
|
|
28
30
|
view: 'code',
|
|
@@ -4,13 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.lintCheck = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
7
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
10
|
const index_js_1 = require("../../../config/index.js");
|
|
9
11
|
const execa_js_1 = require("../../../utils/execa.js");
|
|
10
12
|
const index_js_2 = require("../../../utils/index.js");
|
|
13
|
+
const finding_store_js_1 = require("../../finding-store.js");
|
|
11
14
|
const reuse_js_1 = require("../../install/reuse.js");
|
|
12
15
|
const targets_js_1 = require("../../targets.js");
|
|
13
16
|
const types_js_1 = require("../../types.js");
|
|
17
|
+
const locate_js_1 = require("./locate.js");
|
|
18
|
+
const MAX_LOGGED_PROBLEMS = 50;
|
|
14
19
|
/**
|
|
15
20
|
* Resolve the ESLint binary under the client's `install.reuseLocal` policy.
|
|
16
21
|
*
|
|
@@ -36,6 +41,17 @@ async function getESLintMajorVersion(eslintBin) {
|
|
|
36
41
|
return 8;
|
|
37
42
|
}
|
|
38
43
|
}
|
|
44
|
+
function readProblems(reportFile, cwd) {
|
|
45
|
+
try {
|
|
46
|
+
return (0, locate_js_1.parseEslintJson)(node_fs_1.default.readFileSync(reportFile, 'utf-8'), cwd);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
(0, index_js_2.cleanupTempFile)(reportFile);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
39
55
|
exports.lintCheck = {
|
|
40
56
|
name: 'ESLint',
|
|
41
57
|
phase: 'blocking',
|
|
@@ -87,12 +103,23 @@ exports.lintCheck = {
|
|
|
87
103
|
if (!hasLocal && version < 9 && toolRoots.length > 0) {
|
|
88
104
|
args.push('--resolve-plugins-relative-to', toolRoots[0]);
|
|
89
105
|
}
|
|
106
|
+
// JSON to a file, not stylish text to the terminal: the file is what lets a
|
|
107
|
+
// finding name its file, line and rule instead of saying "lint errors found".
|
|
108
|
+
// The same problems are logged below in a compact form, so nothing is lost.
|
|
109
|
+
const reportFile = node_path_1.default.join(node_os_1.default.tmpdir(), `pr-checkmate-eslint-${process.pid}-${Date.now()}.json`);
|
|
110
|
+
args.push('--format', 'json', '--output-file', reportFile);
|
|
90
111
|
const result = await (0, execa_js_1.execa)(process.execPath, [eslintBin, ...args], {
|
|
91
112
|
cwd: ctx.cwd,
|
|
92
113
|
stdio: 'inherit',
|
|
93
114
|
reject: false,
|
|
94
115
|
env: { PR_CHECKMATE_TOOL_ROOTS: toolRoots.join(node_path_1.default.delimiter) },
|
|
95
116
|
});
|
|
117
|
+
const problems = readProblems(reportFile, ctx.cwd);
|
|
118
|
+
problems.slice(0, MAX_LOGGED_PROBLEMS).forEach(f => {
|
|
119
|
+
index_js_2.logger.warn(`[ESLint]: ${f.file}:${f.line ?? 0} ${f.rule} ${f.snippet ?? ''}`);
|
|
120
|
+
});
|
|
121
|
+
if (result.exitCode === 1)
|
|
122
|
+
problems.forEach(finding_store_js_1.addFinding);
|
|
96
123
|
if (result.exitCode === 0)
|
|
97
124
|
return (0, types_js_1.pass)();
|
|
98
125
|
// ESLint exits 1 for lint errors and 2 for a fatal error — a config it cannot
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Finding } from '../../types.js';
|
|
2
|
+
/** `src/a.ts(3,5): error TS2322: Type …` lines of `tsc --pretty false`. */
|
|
3
|
+
export declare function parseTscDiagnostics(output: string): Finding[];
|
|
4
|
+
/** The array `eslint --format json` writes. Anything else yields no findings. */
|
|
5
|
+
export declare function parseEslintJson(raw: string, cwd: string): Finding[];
|
|
6
|
+
/** One path per line, as `prettier --list-different` prints them. */
|
|
7
|
+
export declare function parseListDifferent(stdout: string): Finding[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseTscDiagnostics = parseTscDiagnostics;
|
|
7
|
+
exports.parseEslintJson = parseEslintJson;
|
|
8
|
+
exports.parseListDifferent = parseListDifferent;
|
|
9
|
+
/**
|
|
10
|
+
* Turn what the language tools print into located findings.
|
|
11
|
+
*
|
|
12
|
+
* The three tools disagree on how to say where a problem is, so each gets a small
|
|
13
|
+
* pure parser here and the checks only decide what to do with the result. Without
|
|
14
|
+
* these an agent was told "lint errors found" and nothing else.
|
|
15
|
+
*/
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const MAX_SNIPPET = 160;
|
|
18
|
+
const TSC_DIAGNOSTIC = /^(.+?)\((\d+),\d+\): error (TS\d+): (.*)$/;
|
|
19
|
+
const clip = (text) => text.length > MAX_SNIPPET ? `${text.slice(0, MAX_SNIPPET - 1)}…` : text;
|
|
20
|
+
/** `src/a.ts(3,5): error TS2322: Type …` lines of `tsc --pretty false`. */
|
|
21
|
+
function parseTscDiagnostics(output) {
|
|
22
|
+
const found = [];
|
|
23
|
+
for (const line of output.split('\n')) {
|
|
24
|
+
const m = TSC_DIAGNOSTIC.exec(line.trim());
|
|
25
|
+
if (m) {
|
|
26
|
+
found.push({
|
|
27
|
+
file: m[1].replace(/\\/g, '/'),
|
|
28
|
+
line: Number(m[2]),
|
|
29
|
+
rule: m[3],
|
|
30
|
+
snippet: clip(m[4]),
|
|
31
|
+
severity: 'error',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
/** The array `eslint --format json` writes. Anything else yields no findings. */
|
|
38
|
+
function parseEslintJson(raw, cwd) {
|
|
39
|
+
let report;
|
|
40
|
+
try {
|
|
41
|
+
report = JSON.parse(raw);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(report))
|
|
47
|
+
return [];
|
|
48
|
+
return report.flatMap(file => (file.messages ?? []).map(m => ({
|
|
49
|
+
file: node_path_1.default.relative(cwd, file.filePath).replace(/\\/g, '/'),
|
|
50
|
+
...(m.line !== undefined && { line: m.line }),
|
|
51
|
+
rule: m.ruleId ?? 'parse-error',
|
|
52
|
+
snippet: clip(m.message),
|
|
53
|
+
severity: m.severity === 2 ? 'error' : 'warn',
|
|
54
|
+
})));
|
|
55
|
+
}
|
|
56
|
+
/** One path per line, as `prettier --list-different` prints them. */
|
|
57
|
+
function parseListDifferent(stdout) {
|
|
58
|
+
return stdout
|
|
59
|
+
.split('\n')
|
|
60
|
+
.map(l => l.trim())
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.map(file => ({
|
|
63
|
+
file: file.replace(/\\/g, '/'),
|
|
64
|
+
rule: 'prettier',
|
|
65
|
+
snippet: 'not formatted',
|
|
66
|
+
severity: 'warn',
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
@@ -9,8 +9,10 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
9
9
|
const index_js_1 = require("../../../config/index.js");
|
|
10
10
|
const execa_js_1 = require("../../../utils/execa.js");
|
|
11
11
|
const index_js_2 = require("../../../utils/index.js");
|
|
12
|
+
const finding_store_js_1 = require("../../finding-store.js");
|
|
12
13
|
const targets_js_1 = require("../../targets.js");
|
|
13
14
|
const types_js_1 = require("../../types.js");
|
|
15
|
+
const locate_js_1 = require("./locate.js");
|
|
14
16
|
function loadUserPrettierConfig(cwd) {
|
|
15
17
|
const configPath = node_path_1.default.join(cwd, 'pr-checkmate.json');
|
|
16
18
|
if (!node_fs_1.default.existsSync(configPath))
|
|
@@ -59,7 +61,9 @@ exports.prettierCheck = {
|
|
|
59
61
|
ignorePath = tempIgnore = createTempIgnoreFile(ctx.cwd);
|
|
60
62
|
const hasLocal = (0, index_js_1.hasLocalConfig)(['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js'], ctx.cwd);
|
|
61
63
|
// write=true → format in place; write=false → report-only (`--check`, never mutates files).
|
|
62
|
-
|
|
64
|
+
// `--list-different` names the unformatted files on stdout, which is what lets a
|
|
65
|
+
// finding point at them; `--check` only says how many there are.
|
|
66
|
+
const mode = ctx.write ? '--write' : '--list-different';
|
|
63
67
|
let tempConfig = null;
|
|
64
68
|
let args;
|
|
65
69
|
if (hasLocal) {
|
|
@@ -73,11 +77,20 @@ exports.prettierCheck = {
|
|
|
73
77
|
try {
|
|
74
78
|
const result = await (0, execa_js_1.execa)(process.execPath, [prettierBin, ...args], {
|
|
75
79
|
cwd: ctx.cwd,
|
|
76
|
-
stdio: 'inherit',
|
|
80
|
+
...(ctx.write && { stdio: 'inherit' }),
|
|
77
81
|
reject: false,
|
|
78
82
|
});
|
|
79
83
|
if (result.exitCode === 0)
|
|
80
84
|
return (0, types_js_1.pass)();
|
|
85
|
+
if (!ctx.write) {
|
|
86
|
+
const unformatted = (0, locate_js_1.parseListDifferent)(String(result.stdout ?? ''));
|
|
87
|
+
unformatted.forEach(f => {
|
|
88
|
+
index_js_2.logger.warn(`[Prettier]: not formatted: ${f.file}`);
|
|
89
|
+
(0, finding_store_js_1.addFinding)(f);
|
|
90
|
+
});
|
|
91
|
+
if (result.stderr)
|
|
92
|
+
index_js_2.logger.warn(String(result.stderr));
|
|
93
|
+
}
|
|
81
94
|
// In --check mode a non-zero exit means "some files are not formatted".
|
|
82
95
|
return (0, types_js_1.warn)(ctx.write ? 'prettier failed' : 'unformatted files (run with write to fix)');
|
|
83
96
|
}
|
|
@@ -8,8 +8,10 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const execa_js_1 = require("../../../utils/execa.js");
|
|
10
10
|
const index_js_1 = require("../../../utils/index.js");
|
|
11
|
+
const finding_store_js_1 = require("../../finding-store.js");
|
|
11
12
|
const reuse_js_1 = require("../../install/reuse.js");
|
|
12
13
|
const types_js_1 = require("../../types.js");
|
|
14
|
+
const locate_js_1 = require("./locate.js");
|
|
13
15
|
/**
|
|
14
16
|
* Prefer the consumer project's own TypeScript so its tsconfig is compiled by the
|
|
15
17
|
* version it was written for; fall back to the compiler bundled with pr-checkmate.
|
|
@@ -45,7 +47,7 @@ exports.typecheckCheck = {
|
|
|
45
47
|
return (0, types_js_1.skip)('project dependencies not installed (no node_modules) — run `npm ci` before typecheck');
|
|
46
48
|
}
|
|
47
49
|
const tscBin = resolveTsc(ctx);
|
|
48
|
-
const result = await (0, execa_js_1.execa)(process.execPath, [tscBin, '--noEmit'], {
|
|
50
|
+
const result = await (0, execa_js_1.execa)(process.execPath, [tscBin, '--noEmit', '--pretty', 'false'], {
|
|
49
51
|
cwd: ctx.cwd,
|
|
50
52
|
reject: false,
|
|
51
53
|
});
|
|
@@ -62,6 +64,7 @@ exports.typecheckCheck = {
|
|
|
62
64
|
index_js_1.logger.warn(`[TypeScript]: ⚠️ tsc exited ${result.exitCode ?? 'abnormally'} without reporting a type error — treating as a tooling failure, not a type error`);
|
|
63
65
|
return (0, types_js_1.warn)(`tsc failed to run (exit ${result.exitCode ?? 'killed'})`);
|
|
64
66
|
}
|
|
67
|
+
(0, locate_js_1.parseTscDiagnostics)(output).forEach(finding_store_js_1.addFinding);
|
|
65
68
|
return (0, types_js_1.fail)(`${errorCount} type error(s)`);
|
|
66
69
|
},
|
|
67
70
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Document, LineCounter } from 'yaml';
|
|
1
2
|
import { Check } from '../../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* True when a parser problem describes something actually wrong with the file —
|
|
@@ -6,6 +7,20 @@ import { Check } from '../../types.js';
|
|
|
6
7
|
export declare function isFileDefect(problem: {
|
|
7
8
|
code?: string;
|
|
8
9
|
}): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Aliases (`*name`) that point at an anchor which does not exist before them.
|
|
12
|
+
*
|
|
13
|
+
* The parser does not treat this as a parse error: it builds the alias node and only
|
|
14
|
+
* throws when the document is converted to a JS value, which is exactly what a CI
|
|
15
|
+
* runner does next. So `doc.errors` stays empty for a pipeline that reuses a step via
|
|
16
|
+
* `*update-knowledge` after its `&update-knowledge` anchor was deleted, and the check
|
|
17
|
+
* used to pass a file Bitbucket rejects. Found by walking the alias nodes.
|
|
18
|
+
*/
|
|
19
|
+
export declare function unresolvedAliases(doc: Document.Parsed, lineCounter: LineCounter): {
|
|
20
|
+
name: string;
|
|
21
|
+
line: number;
|
|
22
|
+
col: number;
|
|
23
|
+
}[];
|
|
9
24
|
/**
|
|
10
25
|
* Bundled YAML linter (the `yaml` package — no runner dependency, always
|
|
11
26
|
* available like Markdown/Spellcheck). Parses every changed `.yml`/`.yaml` file
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.yamlLintCheck = void 0;
|
|
7
7
|
exports.isFileDefect = isFileDefect;
|
|
8
|
+
exports.unresolvedAliases = unresolvedAliases;
|
|
8
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
11
|
const yaml_1 = require("yaml");
|
|
@@ -38,6 +39,27 @@ const NON_DEFECT_CODES = new Set(['TAG_RESOLVE_FAILED']);
|
|
|
38
39
|
function isFileDefect(problem) {
|
|
39
40
|
return problem.code === undefined || !NON_DEFECT_CODES.has(problem.code);
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Aliases (`*name`) that point at an anchor which does not exist before them.
|
|
44
|
+
*
|
|
45
|
+
* The parser does not treat this as a parse error: it builds the alias node and only
|
|
46
|
+
* throws when the document is converted to a JS value, which is exactly what a CI
|
|
47
|
+
* runner does next. So `doc.errors` stays empty for a pipeline that reuses a step via
|
|
48
|
+
* `*update-knowledge` after its `&update-knowledge` anchor was deleted, and the check
|
|
49
|
+
* used to pass a file Bitbucket rejects. Found by walking the alias nodes.
|
|
50
|
+
*/
|
|
51
|
+
function unresolvedAliases(doc, lineCounter) {
|
|
52
|
+
const found = [];
|
|
53
|
+
(0, yaml_1.visit)(doc, {
|
|
54
|
+
Alias(_key, node) {
|
|
55
|
+
if (node.resolve(doc) !== undefined)
|
|
56
|
+
return;
|
|
57
|
+
const { line, col } = lineCounter.linePos(node.range?.[0] ?? 0);
|
|
58
|
+
found.push({ name: node.source, line, col });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
41
63
|
/**
|
|
42
64
|
* Bundled YAML linter (the `yaml` package — no runner dependency, always
|
|
43
65
|
* available like Markdown/Spellcheck). Parses every changed `.yml`/`.yaml` file
|
|
@@ -81,8 +103,18 @@ exports.yamlLintCheck = {
|
|
|
81
103
|
}
|
|
82
104
|
// parseAllDocuments never throws — it collects problems on each document,
|
|
83
105
|
// so a single malformed file cannot abort the whole check.
|
|
84
|
-
const
|
|
106
|
+
const lineCounter = new yaml_1.LineCounter();
|
|
107
|
+
const docs = (0, yaml_1.parseAllDocuments)(source, { prettyErrors: true, lineCounter });
|
|
85
108
|
for (const doc of docs) {
|
|
109
|
+
for (const alias of unresolvedAliases(doc, lineCounter)) {
|
|
110
|
+
issues.push({
|
|
111
|
+
file,
|
|
112
|
+
blocking: true,
|
|
113
|
+
line: alias.line,
|
|
114
|
+
col: alias.col,
|
|
115
|
+
message: `Unresolved alias "*${alias.name}" (the anchor must be declared before the alias)`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
86
118
|
const problems = [
|
|
87
119
|
...doc.errors.map(p => ({ problem: p, blocking: true })),
|
|
88
120
|
...doc.warnings.map(p => ({ problem: p, blocking: false })),
|
package/dist/cjs/core/sarif.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Only what a consumer can use is written. A failed or warned check that located
|
|
7
7
|
* its problems yields one result per location; one that could not yields a single
|
|
8
8
|
* result with no location, because a finding with nowhere to point is still a
|
|
9
|
-
* finding.
|
|
9
|
+
* finding. GitHub code scanning displays only results that have a location, so
|
|
10
|
+
* those one-line summaries reach editors and other viewers, not its alerts.
|
|
11
|
+
* Passes and skips are not results.
|
|
10
12
|
*/
|
|
11
13
|
import type { CheckReport } from './types.js';
|
|
12
14
|
/** Convert a report to a SARIF log object, ready for `JSON.stringify`. */
|
|
@@ -5,7 +5,7 @@ export type Ecosystem = 'npm' | 'pypi';
|
|
|
5
5
|
* `ok` — nothing found
|
|
6
6
|
* `unknown` — a lookup failed, so nothing can be said
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
type Verdict = 'avoid' | 'review' | 'ok' | 'unknown';
|
|
9
9
|
export interface DependencyReport {
|
|
10
10
|
name: string;
|
|
11
11
|
ecosystem: Ecosystem;
|
|
@@ -31,3 +31,4 @@ export declare function checkDependency(name: string, opts?: {
|
|
|
31
31
|
}): Promise<DependencyReport>;
|
|
32
32
|
/** The report as the text an agent reads. */
|
|
33
33
|
export declare function formatDependency(r: DependencyReport): string;
|
|
34
|
+
export {};
|
|
@@ -168,21 +168,37 @@ const daysSince = (iso) => {
|
|
|
168
168
|
const t = typeof iso === 'string' ? Date.parse(iso) : NaN;
|
|
169
169
|
return Number.isNaN(t) ? undefined : Math.floor((Date.now() - t) / DAY_MS);
|
|
170
170
|
};
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Record why a registry answer cannot be used: "no such package" is fatal, an answer
|
|
173
|
+
* that is missing or unreadable only leaves the report incomplete. Returns `true`
|
|
174
|
+
* when the lookup should stop; `false` means `res.body` is a JSON object.
|
|
175
|
+
*/
|
|
176
|
+
function rejectRegistryAnswer(out, res, registry) {
|
|
174
177
|
if (res.status === 404) {
|
|
175
178
|
out.fatal = true;
|
|
176
179
|
out.facts.exists = false;
|
|
177
|
-
out.reasons.push(
|
|
178
|
-
return
|
|
180
|
+
out.reasons.push(`not on ${registry} — a name that sounds right but does not exist is how hallucinated packages get squatted; do not add it`);
|
|
181
|
+
return true;
|
|
179
182
|
}
|
|
180
183
|
if (!res.ok || !isRecord(res.body)) {
|
|
181
184
|
out.incomplete = true;
|
|
182
|
-
out.reasons.push(`could not read
|
|
183
|
-
return
|
|
185
|
+
out.reasons.push(`could not read ${registry} (${res.error ?? res.status})`);
|
|
186
|
+
return true;
|
|
184
187
|
}
|
|
185
|
-
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
/** Fetch a registry document: a fresh `Collected` plus the JSON object, absent when it cannot be used. */
|
|
191
|
+
async function openRegistry(url, get, registry) {
|
|
192
|
+
const out = { reasons: [], facts: {}, fatal: false, incomplete: false };
|
|
193
|
+
const res = await get(url);
|
|
194
|
+
if (rejectRegistryAnswer(out, res, registry))
|
|
195
|
+
return { out };
|
|
196
|
+
return { out, doc: res.body };
|
|
197
|
+
}
|
|
198
|
+
async function lookupNpm(name, requested, get) {
|
|
199
|
+
const { out, doc } = await openRegistry(`https://registry.npmjs.org/${name.replace('/', '%2f')}`, get, 'the npm registry');
|
|
200
|
+
if (!doc)
|
|
201
|
+
return out;
|
|
186
202
|
const tags = isRecord(doc['dist-tags']) ? doc['dist-tags'] : {};
|
|
187
203
|
const versions = isRecord(doc.versions) ? doc.versions : {};
|
|
188
204
|
const time = isRecord(doc.time) ? doc.time : {};
|
|
@@ -233,21 +249,11 @@ async function lookupNpm(name, requested, get) {
|
|
|
233
249
|
return out;
|
|
234
250
|
}
|
|
235
251
|
async function lookupPypi(name, requested, get) {
|
|
236
|
-
const out
|
|
237
|
-
|
|
238
|
-
if (res.status === 404) {
|
|
239
|
-
out.fatal = true;
|
|
240
|
-
out.facts.exists = false;
|
|
241
|
-
out.reasons.push('not on PyPI — a name that sounds right but does not exist is how hallucinated packages get squatted; do not add it');
|
|
252
|
+
const { out, doc } = await openRegistry(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`, get, 'PyPI');
|
|
253
|
+
if (!doc)
|
|
242
254
|
return out;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
out.incomplete = true;
|
|
246
|
-
out.reasons.push(`could not read PyPI (${res.error ?? res.status})`);
|
|
247
|
-
return out;
|
|
248
|
-
}
|
|
249
|
-
const info = isRecord(res.body.info) ? res.body.info : {};
|
|
250
|
-
const releases = isRecord(res.body.releases) ? res.body.releases : {};
|
|
255
|
+
const info = isRecord(doc.info) ? doc.info : {};
|
|
256
|
+
const releases = isRecord(doc.releases) ? doc.releases : {};
|
|
251
257
|
out.facts.exists = true;
|
|
252
258
|
if (requested && !(requested in releases)) {
|
|
253
259
|
out.fatal = true;
|
|
@@ -13,15 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
/** Revisions we can speak, newest first. A client asking for another gets the newest. */
|
|
15
15
|
export declare const SUPPORTED_PROTOCOLS: readonly string[];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
jsonrpc: '2.0';
|
|
19
|
-
/** Absent on notifications, which never get an answer. */
|
|
20
|
-
id?: JsonRpcId;
|
|
21
|
-
method: string;
|
|
22
|
-
params?: Record<string, unknown>;
|
|
23
|
-
}
|
|
24
|
-
export interface JsonRpcResponse {
|
|
16
|
+
type JsonRpcId = string | number | null;
|
|
17
|
+
interface JsonRpcResponse {
|
|
25
18
|
jsonrpc: '2.0';
|
|
26
19
|
id: JsonRpcId;
|
|
27
20
|
result?: unknown;
|
|
@@ -95,3 +88,4 @@ export declare function negotiateProtocol(requested: unknown): string;
|
|
|
95
88
|
export declare function handleMessage(server: ServerDefinition, message: unknown): Promise<JsonRpcResponse | null>;
|
|
96
89
|
/** Decode one stdio line. A line that is not JSON gets a parse error, never a crash. */
|
|
97
90
|
export declare function handleLine(server: ServerDefinition, line: string): Promise<JsonRpcResponse | null>;
|
|
91
|
+
export {};
|
package/dist/cjs/mcp/review.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare function snapshotWorktree(cwd: string): Promise<{
|
|
|
35
35
|
baseSha: string;
|
|
36
36
|
headSha: string;
|
|
37
37
|
} | undefined>;
|
|
38
|
-
|
|
38
|
+
interface ReviewOptions {
|
|
39
39
|
/** Directory inside the repository to review. */
|
|
40
40
|
cwd: string;
|
|
41
41
|
scope?: ReviewScope;
|
|
@@ -49,7 +49,7 @@ export interface ReviewOptions {
|
|
|
49
49
|
*/
|
|
50
50
|
write?: boolean;
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
interface ReviewOutcome {
|
|
53
53
|
report: CheckReport;
|
|
54
54
|
scope: ReviewScope;
|
|
55
55
|
/** Files the diff touches; absent for `full`, which has no diff. */
|
|
@@ -61,3 +61,4 @@ export interface ReviewOutcome {
|
|
|
61
61
|
export declare function review(opts: ReviewOptions): Promise<ReviewOutcome>;
|
|
62
62
|
/** Files in the working-tree diff, with a content hash, so a later call can tell what a fixer rewrote. */
|
|
63
63
|
export declare function fingerprintChanges(cwd: string): Promise<Map<string, string>>;
|
|
64
|
+
export {};
|
package/dist/cjs/mcp/review.js
CHANGED
|
@@ -144,7 +144,7 @@ async function fingerprintChanges(cwd) {
|
|
|
144
144
|
});
|
|
145
145
|
for (const name of stdout.split('\n').filter(Boolean)) {
|
|
146
146
|
const abs = node_path_1.default.join(cwd, name);
|
|
147
|
-
files.set(name, node_fs_1.default.existsSync(abs) ? (0, node_crypto_1.createHash)('
|
|
147
|
+
files.set(name, node_fs_1.default.existsSync(abs) ? (0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(abs)).digest('hex') : '');
|
|
148
148
|
}
|
|
149
149
|
return files;
|
|
150
150
|
}
|
package/dist/cjs/mcp/tools.js
CHANGED
|
@@ -288,6 +288,41 @@ const getRules = {
|
|
|
288
288
|
return serialized(async () => ({ text: await describeRules(cwd) }));
|
|
289
289
|
},
|
|
290
290
|
};
|
|
291
|
+
const SAME_SCOPE_PROPERTY = {
|
|
292
|
+
type: 'string',
|
|
293
|
+
enum: [...review_js_1.REVIEW_SCOPES],
|
|
294
|
+
description: 'Same meaning as in check_changes. Defaults to "worktree".',
|
|
295
|
+
};
|
|
296
|
+
/** The scope a call asks for, or the error to hand back when it is not one we know. */
|
|
297
|
+
function readScope(args) {
|
|
298
|
+
const scope = args.scope ?? 'worktree';
|
|
299
|
+
if (review_js_1.REVIEW_SCOPES.includes(scope))
|
|
300
|
+
return { scope: scope };
|
|
301
|
+
return {
|
|
302
|
+
error: {
|
|
303
|
+
text: `Unknown scope "${String(scope)}". Use one of: ${review_js_1.REVIEW_SCOPES.join(', ')}.`,
|
|
304
|
+
isError: true,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const SCOPE_ONLY_SCHEMA = {
|
|
309
|
+
type: 'object',
|
|
310
|
+
properties: { scope: SAME_SCOPE_PROPERTY, ...CWD_PROPERTY },
|
|
311
|
+
additionalProperties: false,
|
|
312
|
+
};
|
|
313
|
+
/** Review the repository a call points at and format the outcome for the agent. */
|
|
314
|
+
async function reviewFor(args, scope, extras) {
|
|
315
|
+
const cwd = await repoFor(args);
|
|
316
|
+
const outcome = await serialized(() => inDirectory(cwd, () => (0, review_js_1.review)({ cwd, scope, ...extras })));
|
|
317
|
+
return formatReport(outcome.report, outcome);
|
|
318
|
+
}
|
|
319
|
+
/** A review whose only argument is the scope: `scan_secrets` and `verify`. */
|
|
320
|
+
async function reviewWithScope(args, extras) {
|
|
321
|
+
const asked = readScope(args);
|
|
322
|
+
if ('error' in asked)
|
|
323
|
+
return asked.error;
|
|
324
|
+
return reviewFor(args, asked.scope, extras);
|
|
325
|
+
}
|
|
291
326
|
const checkChanges = {
|
|
292
327
|
name: 'check_changes',
|
|
293
328
|
description: "Run the repository's pull-request gate on the code you have changed — uncommitted files included, nothing needs to be staged — and return the findings. " +
|
|
@@ -315,13 +350,9 @@ const checkChanges = {
|
|
|
315
350
|
additionalProperties: false,
|
|
316
351
|
},
|
|
317
352
|
async run(args) {
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
return
|
|
321
|
-
text: `Unknown scope "${String(scope)}". Use one of: ${review_js_1.REVIEW_SCOPES.join(', ')}.`,
|
|
322
|
-
isError: true,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
353
|
+
const asked = readScope(args);
|
|
354
|
+
if ('error' in asked)
|
|
355
|
+
return asked.error;
|
|
325
356
|
const requested = Array.isArray(args.checks) ? args.checks.map(String) : [];
|
|
326
357
|
const known = new Set(registry_js_1.REGISTRY.map(c => c.name));
|
|
327
358
|
const only = requested.map(name => commands_js_1.COMMAND_TO_CHECK[name] ?? name);
|
|
@@ -334,68 +365,22 @@ const checkChanges = {
|
|
|
334
365
|
isError: true,
|
|
335
366
|
};
|
|
336
367
|
}
|
|
337
|
-
|
|
338
|
-
const outcome = await serialized(() => inDirectory(cwd, () => (0, review_js_1.review)({ cwd, scope: scope, only, deep: args.deep === true })));
|
|
339
|
-
return formatReport(outcome.report, outcome);
|
|
368
|
+
return reviewFor(args, asked.scope, { only, deep: args.deep === true });
|
|
340
369
|
},
|
|
341
370
|
};
|
|
342
371
|
const scanSecrets = {
|
|
343
372
|
name: 'scan_secrets',
|
|
344
373
|
description: 'Scan the code you have changed for committed credentials, tokens, private keys and risky additions. Run it before committing configuration, environment or credential files, and whenever a value looks like a secret.',
|
|
345
|
-
inputSchema:
|
|
346
|
-
|
|
347
|
-
properties: {
|
|
348
|
-
scope: {
|
|
349
|
-
type: 'string',
|
|
350
|
-
enum: [...review_js_1.REVIEW_SCOPES],
|
|
351
|
-
description: 'Same meaning as in check_changes. Defaults to "worktree".',
|
|
352
|
-
},
|
|
353
|
-
...CWD_PROPERTY,
|
|
354
|
-
},
|
|
355
|
-
additionalProperties: false,
|
|
356
|
-
},
|
|
357
|
-
async run(args) {
|
|
358
|
-
const scope = args.scope ?? 'worktree';
|
|
359
|
-
if (!review_js_1.REVIEW_SCOPES.includes(scope)) {
|
|
360
|
-
return {
|
|
361
|
-
text: `Unknown scope "${String(scope)}". Use one of: ${review_js_1.REVIEW_SCOPES.join(', ')}.`,
|
|
362
|
-
isError: true,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
const cwd = await repoFor(args);
|
|
366
|
-
const outcome = await serialized(() => inDirectory(cwd, () => (0, review_js_1.review)({ cwd, scope: scope, only: [...SECRET_CHECKS] })));
|
|
367
|
-
return formatReport(outcome.report, outcome);
|
|
368
|
-
},
|
|
374
|
+
inputSchema: SCOPE_ONLY_SCHEMA,
|
|
375
|
+
run: args => reviewWithScope(args, { only: [...SECRET_CHECKS] }),
|
|
369
376
|
};
|
|
370
377
|
const verify = {
|
|
371
378
|
name: 'verify',
|
|
372
379
|
description: 'The final gate. Runs every check the pull-request gate runs, the slow ones included (typecheck, dependency audit, spellcheck, dead code), on the code you have changed. ' +
|
|
373
380
|
'Call it once when you believe the work is done and before you tell the user it is finished. ' +
|
|
374
381
|
'check_changes is the quick loop while you work; a clean verify is the closest you can get to knowing CI will agree.',
|
|
375
|
-
inputSchema:
|
|
376
|
-
|
|
377
|
-
properties: {
|
|
378
|
-
scope: {
|
|
379
|
-
type: 'string',
|
|
380
|
-
enum: [...review_js_1.REVIEW_SCOPES],
|
|
381
|
-
description: 'Same meaning as in check_changes. Defaults to "worktree".',
|
|
382
|
-
},
|
|
383
|
-
...CWD_PROPERTY,
|
|
384
|
-
},
|
|
385
|
-
additionalProperties: false,
|
|
386
|
-
},
|
|
387
|
-
async run(args) {
|
|
388
|
-
const scope = args.scope ?? 'worktree';
|
|
389
|
-
if (!review_js_1.REVIEW_SCOPES.includes(scope)) {
|
|
390
|
-
return {
|
|
391
|
-
text: `Unknown scope "${String(scope)}". Use one of: ${review_js_1.REVIEW_SCOPES.join(', ')}.`,
|
|
392
|
-
isError: true,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
const cwd = await repoFor(args);
|
|
396
|
-
const outcome = await serialized(() => inDirectory(cwd, () => (0, review_js_1.review)({ cwd, scope: scope, deep: true })));
|
|
397
|
-
return formatReport(outcome.report, outcome);
|
|
398
|
-
},
|
|
382
|
+
inputSchema: SCOPE_ONLY_SCHEMA,
|
|
383
|
+
run: args => reviewWithScope(args, { deep: true }),
|
|
399
384
|
};
|
|
400
385
|
/** The only checks that can rewrite code safely: formatting and lint autofixes. */
|
|
401
386
|
const FIXERS = ['Prettier', 'ESLint'];
|