@j0hanz/code-review-analyst-mcp 1.0.3 → 1.2.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 +203 -193
- package/dist/index.js +22 -18
- package/dist/instructions.md +83 -60
- package/dist/lib/context-budget.d.ts +8 -0
- package/dist/lib/context-budget.js +30 -0
- package/dist/lib/diff-budget.d.ts +3 -1
- package/dist/lib/diff-budget.js +16 -13
- package/dist/lib/diff-parser.d.ts +34 -0
- package/dist/lib/diff-parser.js +114 -0
- package/dist/lib/env-config.d.ts +5 -0
- package/dist/lib/env-config.js +24 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +13 -7
- package/dist/lib/gemini-schema.d.ts +3 -1
- package/dist/lib/gemini-schema.js +21 -19
- package/dist/lib/gemini.d.ts +1 -0
- package/dist/lib/gemini.js +264 -115
- package/dist/lib/model-config.d.ts +17 -0
- package/dist/lib/model-config.js +19 -0
- package/dist/lib/tool-factory.d.ts +21 -9
- package/dist/lib/tool-factory.js +277 -63
- package/dist/lib/tool-response.d.ts +9 -2
- package/dist/lib/tool-response.js +28 -11
- package/dist/lib/types.d.ts +7 -2
- package/dist/prompts/index.js +91 -3
- package/dist/resources/index.js +14 -10
- package/dist/schemas/inputs.d.ts +27 -15
- package/dist/schemas/inputs.js +60 -44
- package/dist/schemas/outputs.d.ts +130 -7
- package/dist/schemas/outputs.js +171 -74
- package/dist/server.d.ts +5 -1
- package/dist/server.js +39 -27
- package/dist/tools/analyze-pr-impact.d.ts +2 -0
- package/dist/tools/analyze-pr-impact.js +46 -0
- package/dist/tools/generate-review-summary.d.ts +2 -0
- package/dist/tools/generate-review-summary.js +67 -0
- package/dist/tools/generate-test-plan.d.ts +2 -0
- package/dist/tools/generate-test-plan.js +56 -0
- package/dist/tools/index.js +10 -6
- package/dist/tools/inspect-code-quality.d.ts +4 -0
- package/dist/tools/inspect-code-quality.js +107 -0
- package/dist/tools/suggest-search-replace.d.ts +2 -0
- package/dist/tools/suggest-search-replace.js +46 -0
- package/package.json +3 -2
- package/dist/tools/review-diff.d.ts +0 -2
- package/dist/tools/review-diff.js +0 -41
- package/dist/tools/risk-score.d.ts +0 -2
- package/dist/tools/risk-score.js +0 -33
- package/dist/tools/suggest-patch.d.ts +0 -2
- package/dist/tools/suggest-patch.js +0 -34
package/dist/index.js
CHANGED
|
@@ -4,26 +4,30 @@ import { parseArgs } from 'node:util';
|
|
|
4
4
|
import { getErrorMessage } from './lib/errors.js';
|
|
5
5
|
import { createServer } from './server.js';
|
|
6
6
|
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
7
|
+
const ARG_OPTION_MODEL = 'model';
|
|
8
|
+
const ARG_OPTION_MAX_DIFF_CHARS = 'max-diff-chars';
|
|
9
|
+
const CLI_OPTIONS = {
|
|
10
|
+
[ARG_OPTION_MODEL]: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
short: 'm',
|
|
13
|
+
},
|
|
14
|
+
[ARG_OPTION_MAX_DIFF_CHARS]: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
function setStringEnv(name, value) {
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
process.env[name] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
function parseCommandLineArgs() {
|
|
8
24
|
const { values } = parseArgs({
|
|
9
25
|
args: process.argv.slice(2),
|
|
10
|
-
options:
|
|
11
|
-
model: {
|
|
12
|
-
type: 'string',
|
|
13
|
-
short: 'm',
|
|
14
|
-
},
|
|
15
|
-
'max-diff-chars': {
|
|
16
|
-
type: 'string',
|
|
17
|
-
},
|
|
18
|
-
},
|
|
26
|
+
options: CLI_OPTIONS,
|
|
19
27
|
strict: false,
|
|
20
28
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if (typeof values['max-diff-chars'] === 'string') {
|
|
25
|
-
process.env.MAX_DIFF_CHARS = values['max-diff-chars'];
|
|
26
|
-
}
|
|
29
|
+
setStringEnv('GEMINI_MODEL', values[ARG_OPTION_MODEL]);
|
|
30
|
+
setStringEnv('MAX_DIFF_CHARS', values[ARG_OPTION_MAX_DIFF_CHARS]);
|
|
27
31
|
}
|
|
28
32
|
let shuttingDown = false;
|
|
29
33
|
async function shutdown(server) {
|
|
@@ -31,12 +35,12 @@ async function shutdown(server) {
|
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
shuttingDown = true;
|
|
34
|
-
await server.
|
|
38
|
+
await server.shutdown();
|
|
35
39
|
process.exit(0);
|
|
36
40
|
}
|
|
37
41
|
function registerShutdownHandlers(server) {
|
|
38
42
|
for (const signal of SHUTDOWN_SIGNALS) {
|
|
39
|
-
process.
|
|
43
|
+
process.once(signal, () => {
|
|
40
44
|
void shutdown(server);
|
|
41
45
|
});
|
|
42
46
|
}
|
|
@@ -46,7 +50,7 @@ async function main() {
|
|
|
46
50
|
const server = createServer();
|
|
47
51
|
const transport = new StdioServerTransport();
|
|
48
52
|
registerShutdownHandlers(server);
|
|
49
|
-
await server.connect(transport);
|
|
53
|
+
await server.server.connect(transport);
|
|
50
54
|
}
|
|
51
55
|
main().catch((error) => {
|
|
52
56
|
console.error(`[fatal] ${getErrorMessage(error)}`);
|
package/dist/instructions.md
CHANGED
|
@@ -6,15 +6,16 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
6
6
|
|
|
7
7
|
## CORE CAPABILITY
|
|
8
8
|
|
|
9
|
-
- Domain:
|
|
10
|
-
- Primary Resources: Unified diff text, structured JSON review results,
|
|
11
|
-
- Tools: READ: `
|
|
9
|
+
- Domain: Gemini-powered code review analysis — accepts unified diffs and returns structured findings, impact assessments, test plans, and search/replace fixes.
|
|
10
|
+
- Primary Resources: Unified diff text, structured JSON review results, impact assessments, test plans.
|
|
11
|
+
- Tools: READ: `analyze_pr_impact`, `generate_review_summary`, `inspect_code_quality`, `suggest_search_replace`, `generate_test_plan`. WRITE: none.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
## PROMPTS
|
|
16
16
|
|
|
17
17
|
- `get-help`: Returns these instructions for quick recall.
|
|
18
|
+
- `review-guide`: Guided workflow for a specific tool and focus area. Accepts `tool` and `focusArea` arguments with auto-completion.
|
|
18
19
|
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -27,12 +28,12 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
27
28
|
## PROGRESS & TASKS
|
|
28
29
|
|
|
29
30
|
- Include `_meta.progressToken` in requests to receive `notifications/progress` updates during Gemini processing.
|
|
30
|
-
- Task-augmented tool calls are supported for
|
|
31
|
-
- These tools declare `execution.taskSupport: "optional"` — invoke normally or as a task.
|
|
31
|
+
- Task-augmented tool calls are supported for all five tools:
|
|
32
32
|
- Send `tools/call` with `task` to get a task id.
|
|
33
33
|
- Poll `tasks/get` and fetch results via `tasks/result`.
|
|
34
34
|
- Use `tasks/cancel` to abort.
|
|
35
|
-
-
|
|
35
|
+
- Progress reports 4 steps: start → input validated → prompt prepared → model response received → completed.
|
|
36
|
+
- Task data is stored in memory (30-minute TTL) and cleared on restart.
|
|
36
37
|
|
|
37
38
|
---
|
|
38
39
|
|
|
@@ -40,87 +41,109 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
40
41
|
|
|
41
42
|
### WORKFLOW A: FULL PR REVIEW
|
|
42
43
|
|
|
43
|
-
1. Call `
|
|
44
|
-
2.
|
|
45
|
-
3.
|
|
46
|
-
NOTE:
|
|
44
|
+
1. Call `generate_review_summary` with `diff` and `repository` to get a high-level summary, risk rating, and merge recommendation.
|
|
45
|
+
2. Call `inspect_code_quality` with the same `diff`, `repository`, and optionally `files` for context-aware deep review.
|
|
46
|
+
3. For each actionable finding, call `suggest_search_replace` with the `diff`, `findingTitle`, and `findingDetails` from step 2.
|
|
47
|
+
NOTE: Keep `suggest_search_replace` scoped to one finding per call. Pre-check diff size < 120,000 chars before any call.
|
|
47
48
|
|
|
48
|
-
### WORKFLOW B:
|
|
49
|
+
### WORKFLOW B: IMPACT ASSESSMENT
|
|
49
50
|
|
|
50
|
-
1. Call `
|
|
51
|
-
2.
|
|
52
|
-
|
|
53
|
-
NOTE: Call `review_diff` first if you need file-level defect evidence.
|
|
51
|
+
1. Call `analyze_pr_impact` with `diff` and `repository` to get severity, categories, breaking changes, and rollback complexity.
|
|
52
|
+
2. Call `generate_review_summary` with the same `diff` for a complementary merge recommendation.
|
|
53
|
+
NOTE: Use `analyze_pr_impact` when you need categorization (breaking_change, api_change, etc.) and rollback assessment.
|
|
54
54
|
|
|
55
|
-
### WORKFLOW C: PATCH FROM
|
|
55
|
+
### WORKFLOW C: PATCH FROM FINDING
|
|
56
56
|
|
|
57
|
-
1. Call `
|
|
58
|
-
2. Call `
|
|
59
|
-
3.
|
|
60
|
-
NOTE:
|
|
57
|
+
1. Call `inspect_code_quality` with `diff`, `repository`, and `focusAreas` to identify specific findings.
|
|
58
|
+
2. Pick one finding. Call `suggest_search_replace` with the same `diff`, plus `findingTitle` and `findingDetails` from that finding.
|
|
59
|
+
3. Validate the returned `blocks[]` before applying — `search` text must match file content exactly.
|
|
60
|
+
NOTE: Never batch multiple findings into one `suggest_search_replace` call.
|
|
61
|
+
|
|
62
|
+
### WORKFLOW D: TEST COVERAGE
|
|
63
|
+
|
|
64
|
+
1. Call `generate_test_plan` with `diff`, `repository`, and optionally `testFramework` and `maxTestCases`.
|
|
65
|
+
2. Review `testCases[]` ordered by priority: `must_have` → `should_have` → `nice_to_have`.
|
|
66
|
+
NOTE: Combine with `inspect_code_quality` when you need finding-aware test targeting.
|
|
61
67
|
|
|
62
68
|
---
|
|
63
69
|
|
|
64
70
|
## TOOL NUANCES & GOTCHAS
|
|
65
71
|
|
|
66
|
-
`
|
|
72
|
+
`analyze_pr_impact`
|
|
73
|
+
|
|
74
|
+
- Purpose: Assess impact severity, categories, breaking changes, and rollback complexity for a PR diff.
|
|
75
|
+
- Input: `diff` (required), `repository` (required), `language` (optional, defaults to auto-detect).
|
|
76
|
+
- Output: `severity`, `categories[]`, `breakingChanges[]`, `affectedAreas[]`, `rollbackComplexity`.
|
|
77
|
+
- Side effects: Calls external Gemini API (Flash model); does not mutate local state.
|
|
78
|
+
|
|
79
|
+
`generate_review_summary`
|
|
67
80
|
|
|
68
|
-
- Purpose:
|
|
69
|
-
- Input: `
|
|
70
|
-
- Output: `
|
|
71
|
-
- Gotcha:
|
|
72
|
-
- Side effects: Calls external Gemini API (`openWorldHint: true`); does not mutate local state (`readOnlyHint: true`).
|
|
81
|
+
- Purpose: Produce a concise PR summary with risk rating, key changes, and merge recommendation.
|
|
82
|
+
- Input: `diff` (required), `repository` (required), `language` (optional).
|
|
83
|
+
- Output: `summary`, `overallRisk`, `keyChanges[]`, `recommendation`, `stats` (computed locally from diff, not by Gemini).
|
|
84
|
+
- Gotcha: `stats` (filesChanged, linesAdded, linesRemoved) are computed from diff parsing before the Gemini call — they are always accurate.
|
|
73
85
|
|
|
74
|
-
`
|
|
86
|
+
`inspect_code_quality`
|
|
75
87
|
|
|
76
|
-
- Purpose:
|
|
77
|
-
- Input: `
|
|
78
|
-
- Output: `
|
|
79
|
-
- Gotcha: Uses
|
|
80
|
-
-
|
|
88
|
+
- Purpose: Deep code review with optional full file context for cross-reference analysis.
|
|
89
|
+
- Input: `diff` (required), `repository` (required), `language` (optional), `focusAreas` (optional, 1–12 items), `maxFindings` (optional, 1–25), `files` (optional, 1–20 files with `path` and `content`).
|
|
90
|
+
- Output: `summary`, `overallRisk`, `findings[]`, `testsNeeded[]`, `contextualInsights[]`, `totalFindings`.
|
|
91
|
+
- Gotcha: Uses Pro model with thinking — slower but higher quality than Flash-based tools. Timeout is 120 seconds.
|
|
92
|
+
- Gotcha: Combined diff + file context must stay under `MAX_CONTEXT_CHARS` (default 500,000). Provide only relevant files.
|
|
93
|
+
- Gotcha: `maxFindings` caps results AFTER Gemini returns. `totalFindings` shows the pre-cap count.
|
|
94
|
+
- Limits: Max 20 files, each max 100,000 chars content.
|
|
81
95
|
|
|
82
|
-
`
|
|
96
|
+
`suggest_search_replace`
|
|
83
97
|
|
|
84
|
-
- Purpose: Generate
|
|
85
|
-
- Input: `
|
|
86
|
-
- Output: `
|
|
87
|
-
- Gotcha:
|
|
88
|
-
-
|
|
98
|
+
- Purpose: Generate verbatim search-and-replace blocks to fix one specific finding.
|
|
99
|
+
- Input: `diff` (required), `findingTitle` (3–160 chars), `findingDetails` (10–3,000 chars).
|
|
100
|
+
- Output: `summary`, `blocks[]` (each with `file`, `search`, `replace`, `explanation`), `validationChecklist[]`.
|
|
101
|
+
- Gotcha: Uses Pro model with thinking (120s timeout). `search` blocks must match exact verbatim text in the file.
|
|
102
|
+
- Gotcha: Scope each call to one finding. Multi-finding calls produce mixed patch intent.
|
|
103
|
+
|
|
104
|
+
`generate_test_plan`
|
|
105
|
+
|
|
106
|
+
- Purpose: Create an actionable test plan with pseudocode covering changes in the diff.
|
|
107
|
+
- Input: `diff` (required), `repository` (required), `language` (optional), `testFramework` (optional, defaults to auto-detect), `maxTestCases` (optional, 1–30).
|
|
108
|
+
- Output: `summary`, `testCases[]` (each with `name`, `type`, `file`, `description`, `pseudoCode`, `priority`), `coverageSummary`.
|
|
109
|
+
- Gotcha: `maxTestCases` caps results AFTER Gemini returns. Uses Flash model with thinking budget.
|
|
89
110
|
|
|
90
111
|
---
|
|
91
112
|
|
|
92
113
|
## CROSS-FEATURE RELATIONSHIPS
|
|
93
114
|
|
|
94
|
-
- Use `
|
|
95
|
-
- Use `
|
|
96
|
-
- All tools share the same
|
|
115
|
+
- Use `inspect_code_quality` findings (`title` + `explanation`) as `findingTitle` + `findingDetails` for `suggest_search_replace`.
|
|
116
|
+
- Use `generate_review_summary` for quick triage before committing to the slower `inspect_code_quality`.
|
|
117
|
+
- All tools share the same diff budget guard, Gemini client, retry policy, and concurrency limiter.
|
|
118
|
+
- `inspect_code_quality` is the only tool that accepts `files` for full file context — all others analyze diff only.
|
|
97
119
|
- All tool responses include both `structuredContent` and JSON-string `content` for client compatibility.
|
|
98
120
|
|
|
99
121
|
---
|
|
100
122
|
|
|
101
123
|
## CONSTRAINTS & LIMITATIONS
|
|
102
124
|
|
|
103
|
-
- Transport: stdio only
|
|
104
|
-
- API credentials: Require `GEMINI_API_KEY` or `GOOGLE_API_KEY
|
|
105
|
-
- Model selection:
|
|
106
|
-
- Diff size: Runtime limit defaults to 120,000 chars (`MAX_DIFF_CHARS` env override).
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
125
|
+
- Transport: stdio only.
|
|
126
|
+
- API credentials: Require `GEMINI_API_KEY` or `GOOGLE_API_KEY` environment variable.
|
|
127
|
+
- Model selection: `GEMINI_MODEL` env var overrides the default (gemini-2.5-flash). Pro model tools (`inspect_code_quality`, `suggest_search_replace`) always use gemini-2.5-pro regardless.
|
|
128
|
+
- Diff size: Runtime limit defaults to 120,000 chars (`MAX_DIFF_CHARS` env override).
|
|
129
|
+
- Context size: Combined diff + files limit defaults to 500,000 chars (`MAX_CONTEXT_CHARS` env override). Only applies to `inspect_code_quality`.
|
|
130
|
+
- Timeout: 60 seconds default (Flash tools), 120 seconds for Pro tools. Retry count: 1 with exponential backoff.
|
|
131
|
+
- Max output tokens: 16,384 per Gemini call.
|
|
132
|
+
- Concurrency: `MAX_CONCURRENT_CALLS` defaults to 10. Excess calls wait up to `MAX_CONCURRENT_CALLS_WAIT_MS` (default 2,000ms).
|
|
133
|
+
- Safety: Gemini safety thresholds default to `BLOCK_NONE`. Override with `GEMINI_HARM_BLOCK_THRESHOLD` (`BLOCK_NONE`, `BLOCK_ONLY_HIGH`, `BLOCK_MEDIUM_AND_ABOVE`, `BLOCK_LOW_AND_ABOVE`).
|
|
134
|
+
- Task TTL: 30 minutes. Task data is in-memory and lost on process restart.
|
|
112
135
|
|
|
113
136
|
---
|
|
114
137
|
|
|
115
138
|
## ERROR HANDLING STRATEGY
|
|
116
139
|
|
|
117
|
-
- `E_INPUT_TOO_LARGE`: Diff exceeded
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
120
|
-
- `
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
-
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
- `E_INPUT_TOO_LARGE`: Diff or combined context exceeded budget. → Split the diff into smaller chunks or reduce the number of `files`. Not retryable.
|
|
141
|
+
- `E_ANALYZE_IMPACT`: Impact analysis failed. → Check API key env vars, reduce diff size, and retry. Inspect `error.kind` for classification.
|
|
142
|
+
- `E_REVIEW_SUMMARY`: Summary generation failed. → Check connectivity/model availability and retry with same diff.
|
|
143
|
+
- `E_INSPECT_QUALITY`: Code quality inspection failed. → Reduce diff size or file context, verify API key, and retry.
|
|
144
|
+
- `E_SUGGEST_SEARCH_REPLACE`: Search/replace generation failed. → Verify finding inputs are specific and retry with narrower details.
|
|
145
|
+
- `E_GENERATE_TEST_PLAN`: Test plan generation failed. → Reduce diff size and retry.
|
|
146
|
+
- Error `kind` values: `validation` (bad input, not retryable), `budget` (size exceeded, not retryable), `upstream` (Gemini API error, retryable), `timeout` (exceeded deadline, retryable), `cancelled` (request aborted, not retryable), `internal` (unexpected, not retryable).
|
|
147
|
+
- Missing API key: Set `GEMINI_API_KEY` or `GOOGLE_API_KEY` env var and restart.
|
|
148
|
+
- Gemini timeout: Reduce diff/context size or increase timeout via tool config.
|
|
149
|
+
- Empty model response: Retry — Gemini occasionally returns empty bodies under load.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createErrorToolResponse } from './tool-response.js';
|
|
2
|
+
interface FileContent {
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function resetMaxContextCharsCacheForTesting(): void;
|
|
6
|
+
export declare function computeContextSize(diff: string, files?: FileContent[]): number;
|
|
7
|
+
export declare function validateContextBudget(diff: string, files?: FileContent[]): ReturnType<typeof createErrorToolResponse> | undefined;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createCachedEnvInt } from './env-config.js';
|
|
2
|
+
import { createErrorToolResponse } from './tool-response.js';
|
|
3
|
+
const DEFAULT_MAX_CONTEXT_CHARS = 500_000;
|
|
4
|
+
const MAX_CONTEXT_CHARS_ENV_VAR = 'MAX_CONTEXT_CHARS';
|
|
5
|
+
const BUDGET_ERROR_META = { retryable: false, kind: 'budget' };
|
|
6
|
+
const contextCharsConfig = createCachedEnvInt(MAX_CONTEXT_CHARS_ENV_VAR, DEFAULT_MAX_CONTEXT_CHARS);
|
|
7
|
+
export function resetMaxContextCharsCacheForTesting() {
|
|
8
|
+
contextCharsConfig.reset();
|
|
9
|
+
}
|
|
10
|
+
function getMaxContextChars() {
|
|
11
|
+
return contextCharsConfig.get();
|
|
12
|
+
}
|
|
13
|
+
export function computeContextSize(diff, files) {
|
|
14
|
+
if (!files || files.length === 0) {
|
|
15
|
+
return diff.length;
|
|
16
|
+
}
|
|
17
|
+
let fileSize = 0;
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
fileSize += file.content.length;
|
|
20
|
+
}
|
|
21
|
+
return diff.length + fileSize;
|
|
22
|
+
}
|
|
23
|
+
export function validateContextBudget(diff, files) {
|
|
24
|
+
const size = computeContextSize(diff, files);
|
|
25
|
+
const max = getMaxContextChars();
|
|
26
|
+
if (size > max) {
|
|
27
|
+
return createErrorToolResponse('E_INPUT_TOO_LARGE', `Combined context size ${size} chars exceeds limit of ${max} chars.`, { providedChars: size, maxChars: max }, BUDGET_ERROR_META);
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createErrorToolResponse } from './tool-response.js';
|
|
2
|
+
export declare function getMaxDiffChars(): number;
|
|
3
|
+
export declare function resetMaxDiffCharsCacheForTesting(): void;
|
|
2
4
|
export declare function exceedsDiffBudget(diff: string): boolean;
|
|
3
|
-
export declare function getDiffBudgetError(diffLength: number): string;
|
|
5
|
+
export declare function getDiffBudgetError(diffLength: number, maxChars?: number): string;
|
|
4
6
|
export declare function validateDiffBudget(diff: string): ReturnType<typeof createErrorToolResponse> | undefined;
|
package/dist/lib/diff-budget.js
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
|
+
import { createCachedEnvInt } from './env-config.js';
|
|
1
2
|
import { createErrorToolResponse } from './tool-response.js';
|
|
2
3
|
const DEFAULT_MAX_DIFF_CHARS = 120_000;
|
|
3
4
|
const MAX_DIFF_CHARS_ENV_VAR = 'MAX_DIFF_CHARS';
|
|
4
5
|
const numberFormatter = new Intl.NumberFormat('en-US');
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const diffCharsConfig = createCachedEnvInt(MAX_DIFF_CHARS_ENV_VAR, DEFAULT_MAX_DIFF_CHARS);
|
|
7
|
+
export function getMaxDiffChars() {
|
|
8
|
+
return diffCharsConfig.get();
|
|
7
9
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
11
|
-
}
|
|
12
|
-
function getMaxDiffChars() {
|
|
13
|
-
return getPositiveIntEnv(MAX_DIFF_CHARS_ENV_VAR) ?? DEFAULT_MAX_DIFF_CHARS;
|
|
10
|
+
export function resetMaxDiffCharsCacheForTesting() {
|
|
11
|
+
diffCharsConfig.reset();
|
|
14
12
|
}
|
|
15
13
|
export function exceedsDiffBudget(diff) {
|
|
16
14
|
return diff.length > getMaxDiffChars();
|
|
17
15
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
function formatDiffBudgetError(diffLength, maxChars) {
|
|
17
|
+
return `diff exceeds max allowed size (${numberFormatter.format(diffLength)} chars > ${numberFormatter.format(maxChars)} chars)`;
|
|
18
|
+
}
|
|
19
|
+
export function getDiffBudgetError(diffLength, maxChars = getMaxDiffChars()) {
|
|
20
|
+
return formatDiffBudgetError(diffLength, maxChars);
|
|
21
21
|
}
|
|
22
|
+
const BUDGET_ERROR_META = { retryable: false, kind: 'budget' };
|
|
22
23
|
export function validateDiffBudget(diff) {
|
|
23
|
-
|
|
24
|
+
const providedChars = diff.length;
|
|
25
|
+
const maxChars = getMaxDiffChars();
|
|
26
|
+
if (providedChars <= maxChars) {
|
|
24
27
|
return undefined;
|
|
25
28
|
}
|
|
26
|
-
return createErrorToolResponse('E_INPUT_TOO_LARGE',
|
|
29
|
+
return createErrorToolResponse('E_INPUT_TOO_LARGE', formatDiffBudgetError(providedChars, maxChars), { providedChars, maxChars }, BUDGET_ERROR_META);
|
|
27
30
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { File as ParsedFile } from 'parse-diff';
|
|
2
|
+
export type { ParsedFile };
|
|
3
|
+
interface DiffStats {
|
|
4
|
+
files: number;
|
|
5
|
+
added: number;
|
|
6
|
+
deleted: number;
|
|
7
|
+
}
|
|
8
|
+
/** Parse unified diff string into structured file list. */
|
|
9
|
+
export declare function parseDiffFiles(diff: string): ParsedFile[];
|
|
10
|
+
export declare function computeDiffStatsAndSummaryFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
11
|
+
stats: DiffStats;
|
|
12
|
+
summary: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function computeDiffStatsAndPathsFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
15
|
+
stats: DiffStats;
|
|
16
|
+
paths: string[];
|
|
17
|
+
}>;
|
|
18
|
+
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
19
|
+
export declare function extractChangedPathsFromFiles(files: readonly ParsedFile[]): string[];
|
|
20
|
+
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
21
|
+
export declare function extractChangedPaths(diff: string): string[];
|
|
22
|
+
export declare function computeDiffStatsFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
23
|
+
files: number;
|
|
24
|
+
added: number;
|
|
25
|
+
deleted: number;
|
|
26
|
+
}>;
|
|
27
|
+
/** Count changed files, added lines, and deleted lines. */
|
|
28
|
+
export declare function computeDiffStats(diff: string): Readonly<{
|
|
29
|
+
files: number;
|
|
30
|
+
added: number;
|
|
31
|
+
deleted: number;
|
|
32
|
+
}>;
|
|
33
|
+
/** Generate human-readable summary of changed files and line counts. */
|
|
34
|
+
export declare function formatFileSummary(files: ParsedFile[]): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import parseDiff from 'parse-diff';
|
|
2
|
+
const UNKNOWN_PATH = 'unknown';
|
|
3
|
+
const NO_FILES_CHANGED = 'No files changed.';
|
|
4
|
+
const EMPTY_PATHS = [];
|
|
5
|
+
const EMPTY_STATS = Object.freeze({ files: 0, added: 0, deleted: 0 });
|
|
6
|
+
const PATH_SORTER = (left, right) => left.localeCompare(right);
|
|
7
|
+
/** Parse unified diff string into structured file list. */
|
|
8
|
+
export function parseDiffFiles(diff) {
|
|
9
|
+
if (!diff) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return parseDiff(diff);
|
|
13
|
+
}
|
|
14
|
+
function cleanPath(path) {
|
|
15
|
+
// Common git diff prefixes
|
|
16
|
+
if (path.startsWith('a/') || path.startsWith('b/')) {
|
|
17
|
+
return path.slice(2);
|
|
18
|
+
}
|
|
19
|
+
return path;
|
|
20
|
+
}
|
|
21
|
+
function resolveChangedPath(file) {
|
|
22
|
+
if (file.to && file.to !== '/dev/null') {
|
|
23
|
+
return cleanPath(file.to);
|
|
24
|
+
}
|
|
25
|
+
if (file.from && file.from !== '/dev/null') {
|
|
26
|
+
return cleanPath(file.from);
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
function sortPaths(paths) {
|
|
31
|
+
if (paths.size === 0) {
|
|
32
|
+
return EMPTY_PATHS;
|
|
33
|
+
}
|
|
34
|
+
return Array.from(paths).sort(PATH_SORTER);
|
|
35
|
+
}
|
|
36
|
+
export function computeDiffStatsAndSummaryFromFiles(files) {
|
|
37
|
+
if (files.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
stats: EMPTY_STATS,
|
|
40
|
+
summary: NO_FILES_CHANGED,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
let added = 0;
|
|
44
|
+
let deleted = 0;
|
|
45
|
+
const summaries = new Array(files.length);
|
|
46
|
+
let index = 0;
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
added += file.additions;
|
|
49
|
+
deleted += file.deletions;
|
|
50
|
+
const path = resolveChangedPath(file);
|
|
51
|
+
summaries[index] =
|
|
52
|
+
`${path ?? UNKNOWN_PATH} (+${file.additions} -${file.deletions})`;
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
stats: { files: files.length, added, deleted },
|
|
57
|
+
summary: `${summaries.join(', ')} [${files.length} files, +${added} -${deleted}]`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function computeDiffStatsAndPathsFromFiles(files) {
|
|
61
|
+
if (files.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
stats: EMPTY_STATS,
|
|
64
|
+
paths: EMPTY_PATHS,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
let added = 0;
|
|
68
|
+
let deleted = 0;
|
|
69
|
+
const paths = new Set();
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
added += file.additions;
|
|
72
|
+
deleted += file.deletions;
|
|
73
|
+
const path = resolveChangedPath(file);
|
|
74
|
+
if (path) {
|
|
75
|
+
paths.add(path);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
stats: { files: files.length, added, deleted },
|
|
80
|
+
paths: sortPaths(paths),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
84
|
+
export function extractChangedPathsFromFiles(files) {
|
|
85
|
+
const paths = new Set();
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
const path = resolveChangedPath(file);
|
|
88
|
+
if (path) {
|
|
89
|
+
paths.add(path);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return sortPaths(paths);
|
|
93
|
+
}
|
|
94
|
+
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
95
|
+
export function extractChangedPaths(diff) {
|
|
96
|
+
return extractChangedPathsFromFiles(parseDiffFiles(diff));
|
|
97
|
+
}
|
|
98
|
+
export function computeDiffStatsFromFiles(files) {
|
|
99
|
+
let added = 0;
|
|
100
|
+
let deleted = 0;
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
added += file.additions;
|
|
103
|
+
deleted += file.deletions;
|
|
104
|
+
}
|
|
105
|
+
return { files: files.length, added, deleted };
|
|
106
|
+
}
|
|
107
|
+
/** Count changed files, added lines, and deleted lines. */
|
|
108
|
+
export function computeDiffStats(diff) {
|
|
109
|
+
return computeDiffStatsFromFiles(parseDiffFiles(diff));
|
|
110
|
+
}
|
|
111
|
+
/** Generate human-readable summary of changed files and line counts. */
|
|
112
|
+
export function formatFileSummary(files) {
|
|
113
|
+
return computeDiffStatsAndSummaryFromFiles(files).summary;
|
|
114
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function parsePositiveInteger(value) {
|
|
2
|
+
const parsed = Number.parseInt(value, 10);
|
|
3
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
return parsed;
|
|
7
|
+
}
|
|
8
|
+
/// Creates a cached integer value from an environment variable, with a default fallback.
|
|
9
|
+
export function createCachedEnvInt(envVar, defaultValue) {
|
|
10
|
+
let cached;
|
|
11
|
+
return {
|
|
12
|
+
get() {
|
|
13
|
+
if (cached !== undefined) {
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
16
|
+
const envValue = process.env[envVar] ?? '';
|
|
17
|
+
cached = parsePositiveInteger(envValue) ?? defaultValue;
|
|
18
|
+
return cached;
|
|
19
|
+
},
|
|
20
|
+
reset() {
|
|
21
|
+
cached = undefined;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/lib/errors.d.ts
CHANGED
package/dist/lib/errors.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
/// Guides for using specific tools in code review analysis.
|
|
3
|
+
export const RETRYABLE_UPSTREAM_ERROR_PATTERN = /(429|500|502|503|504|rate.?limit|quota|overload|unavailable|gateway|timeout|timed.out|connection|reset|econn|enotfound|temporary|transient|invalid.json)/i;
|
|
4
|
+
function isString(value) {
|
|
5
|
+
return typeof value === 'string';
|
|
6
|
+
}
|
|
7
|
+
function hasStringProperty(value, key) {
|
|
8
|
+
if (typeof value !== 'object' || value === null || !(key in value)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const record = value;
|
|
12
|
+
return typeof record[key] === 'string';
|
|
7
13
|
}
|
|
8
14
|
export function getErrorMessage(error) {
|
|
9
|
-
if (
|
|
15
|
+
if (hasStringProperty(error, 'message')) {
|
|
10
16
|
return error.message;
|
|
11
17
|
}
|
|
12
|
-
if (
|
|
18
|
+
if (isString(error)) {
|
|
13
19
|
return error;
|
|
14
20
|
}
|
|
15
21
|
return inspect(error, { depth: 3, breakLength: 120 });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
type JsonRecord = Record<string, unknown>;
|
|
1
2
|
/**
|
|
2
3
|
* Recursively strips value-range constraints (`min*`, `max*`, `multipleOf`)
|
|
3
4
|
* from a JSON Schema object and converts `"type": "integer"` to
|
|
@@ -7,4 +8,5 @@
|
|
|
7
8
|
* same Zod schema that validates tool results. The tool-level result schema
|
|
8
9
|
* enforces strict bounds *after* Gemini returns its response.
|
|
9
10
|
*/
|
|
10
|
-
export declare function stripJsonSchemaConstraints(schema:
|
|
11
|
+
export declare function stripJsonSchemaConstraints(schema: JsonRecord): JsonRecord;
|
|
12
|
+
export {};
|