pr-checkmate 3.3.0 → 4.0.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 (113) hide show
  1. package/LICENSE +8 -3
  2. package/README.md +53 -4
  3. package/dist/cjs/agent/hook.d.ts +37 -0
  4. package/dist/cjs/agent/hook.js +175 -0
  5. package/dist/cjs/agent/registration.d.ts +10 -0
  6. package/dist/cjs/agent/registration.js +28 -0
  7. package/dist/cjs/agent/setup.d.ts +20 -0
  8. package/dist/cjs/agent/setup.js +229 -0
  9. package/dist/cjs/cli.js +88 -0
  10. package/dist/cjs/config/constants.d.ts +2 -0
  11. package/dist/cjs/config/constants.js +5 -1
  12. package/dist/cjs/config/pr-checkmate.template.json +10 -0
  13. package/dist/cjs/core/checks/findings.js +33 -7
  14. package/dist/cjs/core/checks/git/large-files.d.ts +1 -0
  15. package/dist/cjs/core/checks/git/large-files.js +3 -3
  16. package/dist/cjs/core/checks/pr/pr-size-check.d.ts +2 -0
  17. package/dist/cjs/core/checks/pr/pr-size-check.js +5 -5
  18. package/dist/cjs/core/checks/security/agent-instructions.d.ts +4 -0
  19. package/dist/cjs/core/checks/security/agent-instructions.js +149 -0
  20. package/dist/cjs/core/checks/security/diff-security.d.ts +7 -0
  21. package/dist/cjs/core/checks/security/diff-security.js +21 -1
  22. package/dist/cjs/core/checks/security/security.d.ts +0 -13
  23. package/dist/cjs/core/checks/security/security.js +27 -0
  24. package/dist/cjs/core/checks/security/workflow-security.js +33 -0
  25. package/dist/cjs/core/commands.d.ts +6 -0
  26. package/dist/cjs/core/commands.js +11 -1
  27. package/dist/cjs/core/finding-store.d.ts +16 -0
  28. package/dist/cjs/core/finding-store.js +73 -0
  29. package/dist/cjs/core/registry.js +2 -0
  30. package/dist/cjs/core/reporters.js +9 -0
  31. package/dist/cjs/core/run-checks.js +15 -5
  32. package/dist/cjs/core/sarif.d.ts +13 -0
  33. package/dist/cjs/core/sarif.js +60 -0
  34. package/dist/cjs/core/types.d.ts +7 -2
  35. package/dist/cjs/index.d.ts +2 -1
  36. package/dist/cjs/index.js +4 -1
  37. package/dist/cjs/init.js +11 -0
  38. package/dist/cjs/interfaces/index.d.ts +12 -0
  39. package/dist/cjs/mcp/config-tools.d.ts +30 -0
  40. package/dist/cjs/mcp/config-tools.js +320 -0
  41. package/dist/cjs/mcp/dependency.d.ts +33 -0
  42. package/dist/cjs/mcp/dependency.js +407 -0
  43. package/dist/cjs/mcp/protocol.d.ts +97 -0
  44. package/dist/cjs/mcp/protocol.js +181 -0
  45. package/dist/cjs/mcp/review.d.ts +63 -0
  46. package/dist/cjs/mcp/review.js +150 -0
  47. package/dist/cjs/mcp/serve.d.ts +8 -0
  48. package/dist/cjs/mcp/serve.js +49 -0
  49. package/dist/cjs/mcp/settings.d.ts +13 -0
  50. package/dist/cjs/mcp/settings.js +50 -0
  51. package/dist/cjs/mcp/tools.d.ts +23 -0
  52. package/dist/cjs/mcp/tools.js +633 -0
  53. package/dist/cjs/utils/execa.d.ts +7 -0
  54. package/dist/cjs/utils/execa.js +41 -1
  55. package/dist/cjs/utils/result-collector.d.ts +17 -1
  56. package/dist/cjs/utils/result-collector.js +7 -2
  57. package/dist/esm/agent/hook.d.ts +37 -0
  58. package/dist/esm/agent/hook.js +167 -0
  59. package/dist/esm/agent/registration.d.ts +10 -0
  60. package/dist/esm/agent/registration.js +21 -0
  61. package/dist/esm/agent/setup.d.ts +20 -0
  62. package/dist/esm/agent/setup.js +220 -0
  63. package/dist/esm/cli.js +86 -1
  64. package/dist/esm/config/constants.d.ts +2 -0
  65. package/dist/esm/config/constants.js +4 -0
  66. package/dist/esm/config/pr-checkmate.template.json +10 -0
  67. package/dist/esm/core/checks/findings.js +33 -7
  68. package/dist/esm/core/checks/git/large-files.d.ts +1 -0
  69. package/dist/esm/core/checks/git/large-files.js +1 -1
  70. package/dist/esm/core/checks/pr/pr-size-check.d.ts +2 -0
  71. package/dist/esm/core/checks/pr/pr-size-check.js +2 -2
  72. package/dist/esm/core/checks/security/agent-instructions.d.ts +4 -0
  73. package/dist/esm/core/checks/security/agent-instructions.js +146 -0
  74. package/dist/esm/core/checks/security/diff-security.d.ts +7 -0
  75. package/dist/esm/core/checks/security/diff-security.js +20 -0
  76. package/dist/esm/core/checks/security/security.d.ts +0 -13
  77. package/dist/esm/core/checks/security/security.js +27 -0
  78. package/dist/esm/core/checks/security/workflow-security.js +33 -0
  79. package/dist/esm/core/commands.d.ts +6 -0
  80. package/dist/esm/core/commands.js +10 -0
  81. package/dist/esm/core/finding-store.d.ts +16 -0
  82. package/dist/esm/core/finding-store.js +67 -0
  83. package/dist/esm/core/registry.js +2 -0
  84. package/dist/esm/core/reporters.js +9 -0
  85. package/dist/esm/core/run-checks.js +15 -5
  86. package/dist/esm/core/sarif.d.ts +13 -0
  87. package/dist/esm/core/sarif.js +57 -0
  88. package/dist/esm/core/types.d.ts +7 -2
  89. package/dist/esm/index.d.ts +2 -1
  90. package/dist/esm/index.js +2 -0
  91. package/dist/esm/init.js +12 -1
  92. package/dist/esm/interfaces/index.d.ts +12 -0
  93. package/dist/esm/mcp/config-tools.d.ts +30 -0
  94. package/dist/esm/mcp/config-tools.js +310 -0
  95. package/dist/esm/mcp/dependency.d.ts +33 -0
  96. package/dist/esm/mcp/dependency.js +402 -0
  97. package/dist/esm/mcp/protocol.d.ts +97 -0
  98. package/dist/esm/mcp/protocol.js +175 -0
  99. package/dist/esm/mcp/review.d.ts +63 -0
  100. package/dist/esm/mcp/review.js +139 -0
  101. package/dist/esm/mcp/serve.d.ts +8 -0
  102. package/dist/esm/mcp/serve.js +43 -0
  103. package/dist/esm/mcp/settings.d.ts +13 -0
  104. package/dist/esm/mcp/settings.js +43 -0
  105. package/dist/esm/mcp/tools.d.ts +23 -0
  106. package/dist/esm/mcp/tools.js +626 -0
  107. package/dist/esm/utils/execa.d.ts +7 -0
  108. package/dist/esm/utils/execa.js +40 -1
  109. package/dist/esm/utils/result-collector.d.ts +17 -1
  110. package/dist/esm/utils/result-collector.js +7 -2
  111. package/package.json +6 -2
  112. package/pr-checkmate.schema.json +35 -0
  113. package/src/config/pr-checkmate.schema.json +35 -0
@@ -0,0 +1,63 @@
1
+ import type { CheckReport } from '../core/types.js';
2
+ /**
3
+ * What a review looks at.
4
+ *
5
+ * - `worktree` — every uncommitted change, staged or not, new files included.
6
+ * The default: it is the code the agent just wrote.
7
+ * - `staged` — the index only; what `git commit` would record.
8
+ * - `branch` — commits since the branch left the default branch.
9
+ * - `full` — every tracked file.
10
+ */
11
+ export type ReviewScope = 'worktree' | 'staged' | 'branch' | 'full';
12
+ export declare const REVIEW_SCOPES: readonly ReviewScope[];
13
+ /**
14
+ * The checks cheap enough to run after every edit: nothing heavy, nothing that
15
+ * talks to a registry. Derived from the registry rather than listed, so a check
16
+ * added next month is in the loop from day one unless it declares itself heavy.
17
+ */
18
+ export declare function fastCheckNames(): string[];
19
+ /** The repository containing `dir`, or `undefined` when it is not inside one. */
20
+ export declare function resolveRepoRoot(dir: string): Promise<string | undefined>;
21
+ /**
22
+ * Turn the working tree into a git tree object without touching the real index.
23
+ *
24
+ * `git add -A` against a scratch copy of the index, then `write-tree`: the result
25
+ * is the same kind of object `precommit` diffs against, so every delta-aware check
26
+ * works unchanged. The agent's staging area stays exactly as they left it, and
27
+ * `.gitignore` is honoured because git itself does the walking.
28
+ *
29
+ * The one side effect is loose blob objects for the changed files in the object
30
+ * store; `git gc` reclaims them and they are invisible to every other command.
31
+ *
32
+ * @returns base and head to diff, or `undefined` when there is no HEAD yet.
33
+ */
34
+ export declare function snapshotWorktree(cwd: string): Promise<{
35
+ baseSha: string;
36
+ headSha: string;
37
+ } | undefined>;
38
+ export interface ReviewOptions {
39
+ /** Directory inside the repository to review. */
40
+ cwd: string;
41
+ scope?: ReviewScope;
42
+ /** Exact check names to run; when set it overrides {@link ReviewOptions.deep}. */
43
+ only?: string[];
44
+ /** Include heavy and network checks (typecheck, grype, spellcheck, audit…). */
45
+ deep?: boolean;
46
+ /**
47
+ * Let formatters and `--fix` rewrite files. Off everywhere except `apply_fixes`;
48
+ * a review that edits the code it is reviewing has stopped being a review.
49
+ */
50
+ write?: boolean;
51
+ }
52
+ export interface ReviewOutcome {
53
+ report: CheckReport;
54
+ scope: ReviewScope;
55
+ /** Files the diff touches; absent for `full`, which has no diff. */
56
+ changedFiles?: number;
57
+ /** Set when the requested scope could not be built and a fallback ran instead. */
58
+ note?: string;
59
+ }
60
+ /** Run one review, read-only and silent: never writes files, never posts a comment. */
61
+ export declare function review(opts: ReviewOptions): Promise<ReviewOutcome>;
62
+ /** Files in the working-tree diff, with a content hash, so a later call can tell what a fixer rewrote. */
63
+ export declare function fingerprintChanges(cwd: string): Promise<Map<string, string>>;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Running the gate on behalf of an AI agent — the part the MCP server and the
3
+ * commit hook share.
4
+ *
5
+ * Both exist for the same reason: an agent has not committed yet. The CLI's own
6
+ * scopes answer other questions — `precommit` looks at the index, which an agent
7
+ * that never ran `git add` leaves empty, and the default branch scope looks at
8
+ * commits, which is exactly what has not happened. So this module adds the scope
9
+ * an agent actually needs: *everything I have changed so far*.
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { loadProjectConfig } from '../config/loader.js';
16
+ import { REGISTRY } from '../core/registry.js';
17
+ import { runChecks } from '../core/run-checks.js';
18
+ import { execa } from '../utils/execa.js';
19
+ export const REVIEW_SCOPES = ['worktree', 'staged', 'branch', 'full'];
20
+ /**
21
+ * Checks that need the network. A tool call that waits on a registry is a tool
22
+ * call the agent stops trusting, so they sit behind `deep` with the heavy ones.
23
+ */
24
+ const NETWORK_CHECKS = new Set(['NPM Audit', 'Outdated Deps']);
25
+ /**
26
+ * The checks cheap enough to run after every edit: nothing heavy, nothing that
27
+ * talks to a registry. Derived from the registry rather than listed, so a check
28
+ * added next month is in the loop from day one unless it declares itself heavy.
29
+ */
30
+ export function fastCheckNames() {
31
+ return REGISTRY.filter(c => c.weight !== 'heavy' && !NETWORK_CHECKS.has(c.name)).map(c => c.name);
32
+ }
33
+ /** The repository containing `dir`, or `undefined` when it is not inside one. */
34
+ export async function resolveRepoRoot(dir) {
35
+ try {
36
+ const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], { cwd: dir });
37
+ return stdout.trim() || undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ /**
44
+ * Turn the working tree into a git tree object without touching the real index.
45
+ *
46
+ * `git add -A` against a scratch copy of the index, then `write-tree`: the result
47
+ * is the same kind of object `precommit` diffs against, so every delta-aware check
48
+ * works unchanged. The agent's staging area stays exactly as they left it, and
49
+ * `.gitignore` is honoured because git itself does the walking.
50
+ *
51
+ * The one side effect is loose blob objects for the changed files in the object
52
+ * store; `git gc` reclaims them and they are invisible to every other command.
53
+ *
54
+ * @returns base and head to diff, or `undefined` when there is no HEAD yet.
55
+ */
56
+ export async function snapshotWorktree(cwd) {
57
+ let baseSha;
58
+ try {
59
+ baseSha = (await execa('git', ['rev-parse', 'HEAD'], { cwd })).stdout.trim();
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'pr-checkmate-index-'));
65
+ try {
66
+ const indexPath = (await execa('git', ['rev-parse', '--git-path', 'index'], { cwd })).stdout;
67
+ const realIndex = path.resolve(cwd, indexPath.trim());
68
+ const scratchIndex = path.join(scratch, 'index');
69
+ if (fs.existsSync(realIndex))
70
+ fs.copyFileSync(realIndex, scratchIndex);
71
+ const env = { GIT_INDEX_FILE: scratchIndex };
72
+ await execa('git', ['add', '-A'], { cwd, env });
73
+ const headSha = (await execa('git', ['write-tree'], { cwd, env })).stdout.trim();
74
+ return { baseSha, headSha };
75
+ }
76
+ finally {
77
+ fs.rmSync(scratch, { recursive: true, force: true });
78
+ }
79
+ }
80
+ /** Run one review, read-only and silent: never writes files, never posts a comment. */
81
+ export async function review(opts) {
82
+ const scope = opts.scope ?? 'worktree';
83
+ const only = opts.only && opts.only.length > 0 ? opts.only : opts.deep ? undefined : fastCheckNames();
84
+ const base = {
85
+ cwd: opts.cwd,
86
+ config: loadProjectConfig(opts.cwd),
87
+ only,
88
+ reporter: 'none',
89
+ write: opts.write === true,
90
+ };
91
+ if (scope === 'full')
92
+ return { report: await runChecks({ ...base, full: true }), scope };
93
+ if (scope === 'staged')
94
+ return { report: await runChecks({ ...base, staged: true }), scope };
95
+ if (scope === 'branch')
96
+ return { report: await runChecks(base), scope };
97
+ const range = await snapshotWorktree(opts.cwd);
98
+ if (!range) {
99
+ // A repository with no commit has no base to diff against. Falling back to a
100
+ // full scan keeps the file-based checks useful, and the note says so.
101
+ return {
102
+ report: await runChecks({ ...base, full: true }),
103
+ scope: 'full',
104
+ note: 'no commits yet, so there is no diff to review; scanned every tracked file instead',
105
+ };
106
+ }
107
+ const changedFiles = (await execa('git', ['diff', '--name-only', range.baseSha, range.headSha], { cwd: opts.cwd })).stdout
108
+ .split('\n')
109
+ .filter(Boolean).length;
110
+ // Nothing edited yet: say so instead of printing a green report that reads as a
111
+ // verdict on code nobody wrote.
112
+ if (changedFiles === 0) {
113
+ return {
114
+ report: { results: [], summary: { passed: 0, warned: 0, failed: 0, skipped: 0 }, ok: true },
115
+ scope,
116
+ changedFiles,
117
+ };
118
+ }
119
+ return {
120
+ report: await runChecks({ ...base, baseSha: range.baseSha, headSha: range.headSha }),
121
+ scope,
122
+ changedFiles,
123
+ };
124
+ }
125
+ /** Files in the working-tree diff, with a content hash, so a later call can tell what a fixer rewrote. */
126
+ export async function fingerprintChanges(cwd) {
127
+ const range = await snapshotWorktree(cwd);
128
+ const files = new Map();
129
+ if (!range)
130
+ return files;
131
+ const { stdout } = await execa('git', ['diff', '--name-only', range.baseSha, range.headSha], {
132
+ cwd,
133
+ });
134
+ for (const name of stdout.split('\n').filter(Boolean)) {
135
+ const abs = path.join(cwd, name);
136
+ files.set(name, fs.existsSync(abs) ? createHash('sha1').update(fs.readFileSync(abs)).digest('hex') : '');
137
+ }
138
+ return files;
139
+ }
@@ -0,0 +1,8 @@
1
+ import { type ServerDefinition } from './protocol.js';
2
+ /**
3
+ * Run the server until the client closes stdin.
4
+ *
5
+ * Resolves once every request already received has been answered, so a client
6
+ * that sends a call and immediately closes the pipe still gets its response.
7
+ */
8
+ export declare function serve(server: ServerDefinition, input?: NodeJS.ReadableStream): Promise<void>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The stdio transport: one JSON message per line in, one per line out.
3
+ *
4
+ * stdout belongs to the protocol. A single stray byte on it — a log line, a
5
+ * progress message from a tool a check spawned — corrupts the stream and the
6
+ * client drops the server with an error that never mentions the cause. So the
7
+ * first thing `serve` does is take the real `stdout.write` for itself and point
8
+ * everything else at stderr, which MCP clients capture as the server's log.
9
+ */
10
+ import readline from 'node:readline';
11
+ import { isolateInheritedStdio } from '../utils/execa.js';
12
+ import { logger } from '../utils/index.js';
13
+ import { handleLine } from './protocol.js';
14
+ /**
15
+ * Run the server until the client closes stdin.
16
+ *
17
+ * Resolves once every request already received has been answered, so a client
18
+ * that sends a call and immediately closes the pipe still gets its response.
19
+ */
20
+ export async function serve(server, input = process.stdin) {
21
+ const protocolWrite = process.stdout.write.bind(process.stdout);
22
+ const toStderr = ((chunk) => process.stderr.write(chunk));
23
+ process.stdout.write = toStderr;
24
+ isolateInheritedStdio();
25
+ logger.setSink({
26
+ out: chunk => void process.stderr.write(chunk),
27
+ err: chunk => void process.stderr.write(chunk),
28
+ });
29
+ const pending = new Set();
30
+ const lines = readline.createInterface({ input, crlfDelay: Infinity });
31
+ lines.on('line', line => {
32
+ if (!line.trim())
33
+ return;
34
+ const job = handleLine(server, line).then(response => {
35
+ if (response)
36
+ protocolWrite(`${JSON.stringify(response)}\n`);
37
+ });
38
+ pending.add(job);
39
+ void job.finally(() => pending.delete(job));
40
+ });
41
+ await new Promise(resolve => lines.once('close', resolve));
42
+ await Promise.allSettled([...pending]);
43
+ }
@@ -0,0 +1,13 @@
1
+ import type { ToolDefinition } from './protocol.js';
2
+ export interface McpSettings {
3
+ /** `false` leaves the server connected but with no tools, resources or prompts. */
4
+ enabled: boolean;
5
+ /** Tool names to hide from the agent. Unknown names are ignored. */
6
+ disabledTools: string[];
7
+ /** Hide every tool that writes files (`apply_fixes`, `write_config`). */
8
+ readOnly: boolean;
9
+ }
10
+ /** The `mcp` block of the project's `pr-checkmate.json`, with defaults for whatever it leaves out. */
11
+ export declare function readMcpSettings(cwd: string): McpSettings;
12
+ /** The tools the agent may see under `settings`. */
13
+ export declare function allowedTools(tools: ToolDefinition[], settings: McpSettings): ToolDefinition[];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What a project allows the MCP server to do, read from the `mcp` block of its
3
+ * `pr-checkmate.json`.
4
+ *
5
+ * Read again on every request instead of once at startup: the server outlives any
6
+ * single edit, and a user who turns a tool off expects the next call to obey,
7
+ * not the next restart. A missing or unreadable file means the defaults, so a
8
+ * typo in the JSON never silently switches the server off.
9
+ *
10
+ * Kept free of other pr-checkmate modules (type imports only) because the run
11
+ * summary asks the same question, and a heavier import would loop back into it.
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ const DEFAULTS = { enabled: true, disabledTools: [], readOnly: false };
16
+ function isRecord(value) {
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
18
+ }
19
+ /** The `mcp` block of the project's `pr-checkmate.json`, with defaults for whatever it leaves out. */
20
+ export function readMcpSettings(cwd) {
21
+ try {
22
+ const parsed = JSON.parse(fs.readFileSync(path.join(cwd, 'pr-checkmate.json'), 'utf-8'));
23
+ const block = isRecord(parsed) ? parsed.mcp : undefined;
24
+ if (!isRecord(block))
25
+ return DEFAULTS;
26
+ return {
27
+ enabled: block.enabled !== false,
28
+ disabledTools: Array.isArray(block.disabledTools)
29
+ ? block.disabledTools.filter((n) => typeof n === 'string')
30
+ : [],
31
+ readOnly: block.readOnly === true,
32
+ };
33
+ }
34
+ catch {
35
+ return DEFAULTS;
36
+ }
37
+ }
38
+ /** The tools the agent may see under `settings`. */
39
+ export function allowedTools(tools, settings) {
40
+ if (!settings.enabled)
41
+ return [];
42
+ return tools.filter(t => !settings.disabledTools.includes(t.name) && !(settings.readOnly && t.writes));
43
+ }
@@ -0,0 +1,23 @@
1
+ import type { CheckReport } from '../core/types.js';
2
+ import type { ServerDefinition, ToolResult } from './protocol.js';
3
+ import { type ReviewScope } from './review.js';
4
+ /**
5
+ * Render a report for a model: findings first, nothing for what passed.
6
+ *
7
+ * A passing check is the expected case and carries no information the agent can
8
+ * act on, so only the count survives. Skips stay because one of them can mean "the
9
+ * tooling never arrived" — a hole in coverage the agent should be able to see.
10
+ */
11
+ export declare function formatReport(report: CheckReport, meta: {
12
+ scope: ReviewScope;
13
+ changedFiles?: number;
14
+ note?: string;
15
+ advice?: string;
16
+ }): ToolResult;
17
+ /**
18
+ * The server definition `serve` hands to the protocol layer.
19
+ *
20
+ * What it offers is decided per access, from the project's `mcp` settings, so a
21
+ * change to `pr-checkmate.json` takes effect on the agent's next request.
22
+ */
23
+ export declare function createServerDefinition(cwd?: string): ServerDefinition;