@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.
Files changed (50) hide show
  1. package/README.md +203 -193
  2. package/dist/index.js +22 -18
  3. package/dist/instructions.md +83 -60
  4. package/dist/lib/context-budget.d.ts +8 -0
  5. package/dist/lib/context-budget.js +30 -0
  6. package/dist/lib/diff-budget.d.ts +3 -1
  7. package/dist/lib/diff-budget.js +16 -13
  8. package/dist/lib/diff-parser.d.ts +34 -0
  9. package/dist/lib/diff-parser.js +114 -0
  10. package/dist/lib/env-config.d.ts +5 -0
  11. package/dist/lib/env-config.js +24 -0
  12. package/dist/lib/errors.d.ts +1 -0
  13. package/dist/lib/errors.js +13 -7
  14. package/dist/lib/gemini-schema.d.ts +3 -1
  15. package/dist/lib/gemini-schema.js +21 -19
  16. package/dist/lib/gemini.d.ts +1 -0
  17. package/dist/lib/gemini.js +264 -115
  18. package/dist/lib/model-config.d.ts +17 -0
  19. package/dist/lib/model-config.js +19 -0
  20. package/dist/lib/tool-factory.d.ts +21 -9
  21. package/dist/lib/tool-factory.js +277 -63
  22. package/dist/lib/tool-response.d.ts +9 -2
  23. package/dist/lib/tool-response.js +28 -11
  24. package/dist/lib/types.d.ts +7 -2
  25. package/dist/prompts/index.js +91 -3
  26. package/dist/resources/index.js +14 -10
  27. package/dist/schemas/inputs.d.ts +27 -15
  28. package/dist/schemas/inputs.js +60 -44
  29. package/dist/schemas/outputs.d.ts +130 -7
  30. package/dist/schemas/outputs.js +171 -74
  31. package/dist/server.d.ts +5 -1
  32. package/dist/server.js +39 -27
  33. package/dist/tools/analyze-pr-impact.d.ts +2 -0
  34. package/dist/tools/analyze-pr-impact.js +46 -0
  35. package/dist/tools/generate-review-summary.d.ts +2 -0
  36. package/dist/tools/generate-review-summary.js +67 -0
  37. package/dist/tools/generate-test-plan.d.ts +2 -0
  38. package/dist/tools/generate-test-plan.js +56 -0
  39. package/dist/tools/index.js +10 -6
  40. package/dist/tools/inspect-code-quality.d.ts +4 -0
  41. package/dist/tools/inspect-code-quality.js +107 -0
  42. package/dist/tools/suggest-search-replace.d.ts +2 -0
  43. package/dist/tools/suggest-search-replace.js +46 -0
  44. package/package.json +3 -2
  45. package/dist/tools/review-diff.d.ts +0 -2
  46. package/dist/tools/review-diff.js +0 -41
  47. package/dist/tools/risk-score.d.ts +0 -2
  48. package/dist/tools/risk-score.js +0 -33
  49. package/dist/tools/suggest-patch.d.ts +0 -2
  50. 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
- if (typeof values.model === 'string') {
22
- process.env.GEMINI_MODEL = values.model;
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.close();
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.on(signal, () => {
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)}`);
@@ -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: Analyze pull request diffs with Gemini and return structured review findings, risk scores, and focused patch suggestions for automation clients.
10
- - Primary Resources: Unified diff text, structured JSON review results, release-risk assessments, and unified-diff patch suggestions.
11
- - Tools: READ: `review_diff`, `risk_score`, `suggest_patch`. WRITE: none.
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 `review_diff`, `risk_score`, and `suggest_patch`:
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
- - Task data is stored in memory and cleared on restart.
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 `review_diff` with `diff` and `repository` to get structured findings and merge risk.
44
- 2. Use `focusAreas` to bias analysis toward the highest-priority concerns.
45
- 3. Use `maxFindings` to cap result volume when context windows are tight.
46
- NOTE: Never pass oversized diffs. Pre-check against your own limits and handle `E_INPUT_TOO_LARGE`.
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: RELEASE GATE RISK CHECK
49
+ ### WORKFLOW B: IMPACT ASSESSMENT
49
50
 
50
- 1. Call `risk_score` with `diff` to get a 0–100 score, bucket, and rationale.
51
- 2. Set `deploymentCriticality` when evaluating sensitive systems.
52
- 3. Use score and rationale to decide whether to block or require additional validation.
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 A SELECTED FINDING
55
+ ### WORKFLOW C: PATCH FROM FINDING
56
56
 
57
- 1. Call `review_diff` to identify one concrete finding to fix.
58
- 2. Call `suggest_patch` with the same `diff`, plus `findingTitle` and `findingDetails` from that finding.
59
- 3. Use `patchStyle` (`minimal`, `balanced`, `defensive`) to control change breadth.
60
- NOTE: Keep inputs scoped to one finding at a time to avoid mixed patch intent.
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
- `review_diff`
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: Generate structured review findings, overall risk, and test recommendations from a unified diff.
69
- - Input: `maxFindings` defaults to 10; `focusAreas` defaults to security/correctness/regressions/performance when omitted.
70
- - Output: `ok/result/error` envelope; successful payload follows `ReviewDiffResultSchema` and includes `summary`, `overallRisk`, `findings`, and `testsNeeded`.
71
- - Gotcha: Schema allows `diff` up to 400,000 chars, but runtime rejects payloads above `MAX_DIFF_CHARS` (default 120,000) with `E_INPUT_TOO_LARGE`.
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
- `risk_score`
86
+ `inspect_code_quality`
75
87
 
76
- - Purpose: Produce deployment risk score and rationale for release decisions.
77
- - Input: `deploymentCriticality` defaults to `medium` when omitted.
78
- - Output: `ok/result/error` envelope; successful payload includes `score`, `bucket`, and `rationale`.
79
- - Gotcha: Uses the same runtime diff budget guard as other tools; oversized inputs fail before model execution.
80
- - Side effects: External Gemini call only.
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
- `suggest_patch`
96
+ `suggest_search_replace`
83
97
 
84
- - Purpose: Generate a focused unified diff patch for one selected review finding.
85
- - Input: `patchStyle` defaults to `balanced`; requires both `findingTitle` and `findingDetails`.
86
- - Output: `ok/result/error` envelope; successful payload includes `summary`, `patch`, and `validationChecklist`.
87
- - Gotcha: Output is model-generated text and must be validated before application.
88
- - Side effects: External Gemini call only.
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 `review_diff` first to generate concrete finding metadata for `suggest_patch` inputs.
95
- - Use `risk_score` after `review_diff` when you need both defect-level detail and a release gate score.
96
- - All tools share the same Gemini adapter, retry policy, timeout policy, and diff-size guard.
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 in current server entrypoint.
104
- - API credentials: Require `GEMINI_API_KEY` or `GOOGLE_API_KEY`.
105
- - Model selection: Uses `GEMINI_MODEL` if set; defaults to `gemini-2.5-flash`.
106
- - Diff size: Runtime limit defaults to 120,000 chars (`MAX_DIFF_CHARS` env override). Input schema max is 400,000 chars.
107
- - Timeout/retries: Per-call timeout defaults to 45,000 ms; retry count defaults to 1 with exponential backoff.
108
- - Output tokens: `maxOutputTokens` defaults to 16,384 to prevent unbounded responses.
109
- - Safety config: Gemini safety thresholds default to `BLOCK_NONE` for configured harm categories and can be overridden with `GEMINI_HARM_BLOCK_THRESHOLD` (`BLOCK_NONE`, `BLOCK_ONLY_HIGH`, `BLOCK_MEDIUM_AND_ABOVE`, `BLOCK_LOW_AND_ABOVE`).
110
- - Resource scope: Only `internal://instructions` is registered as a resource; no dynamic resource templates are exposed.
111
- - Prompt scope: Only `get-help` is registered.
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 runtime budget. → Split the diff into smaller chunks or raise `MAX_DIFF_CHARS` safely.
118
- - `E_REVIEW_DIFF`: Review generation failed. → Check API key env vars, reduce diff size, and retry; inspect stderr Gemini logs.
119
- - `E_RISK_SCORE`: Risk scoring failed. → Check connectivity/model availability and retry with same diff.
120
- - `E_SUGGEST_PATCH`: Patch generation failed. → Verify finding inputs are specific and retry with narrower details.
121
- - Missing `GEMINI_API_KEY`/`GOOGLE_API_KEY` (wrapped by tool error codes): Credentials not configured. Set one API key env var and rerun.
122
- - Gemini timeout message (`Gemini request timed out after ...ms.`): Request exceeded timeout budget. → Reduce prompt/diff size or increase `timeoutMs` in caller.
123
- - Empty model body (`Gemini returned an empty response body.`): Provider returned no text payload. Retry and inspect model/service status.
124
- - JSON parse failure from model output (wrapped by tool error codes): Output was not valid JSON. Retry with same schema; inspect logs for malformed response text.
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;
@@ -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
- function formatNumber(value) {
6
- return numberFormatter.format(value);
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 getPositiveIntEnv(name) {
9
- const parsed = Number.parseInt(process.env[name] ?? '', 10);
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
- export function getDiffBudgetError(diffLength) {
19
- const maxDiffChars = getMaxDiffChars();
20
- return `diff exceeds max allowed size (${formatNumber(diffLength)} chars > ${formatNumber(maxDiffChars)} chars)`;
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
- if (!exceedsDiffBudget(diff)) {
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', getDiffBudgetError(diff.length));
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,5 @@
1
+ export interface CachedEnvInt {
2
+ get(): number;
3
+ reset(): void;
4
+ }
5
+ export declare function createCachedEnvInt(envVar: string, defaultValue: number): CachedEnvInt;
@@ -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
+ }
@@ -1 +1,2 @@
1
+ export declare const RETRYABLE_UPSTREAM_ERROR_PATTERN: RegExp;
1
2
  export declare function getErrorMessage(error: unknown): string;
@@ -1,15 +1,21 @@
1
1
  import { inspect } from 'node:util';
2
- function isErrorWithMessage(error) {
3
- return (typeof error === 'object' &&
4
- error !== null &&
5
- 'message' in error &&
6
- typeof error.message === 'string');
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 (isErrorWithMessage(error)) {
15
+ if (hasStringProperty(error, 'message')) {
10
16
  return error.message;
11
17
  }
12
- if (typeof error === 'string') {
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: Record<string, unknown>): Record<string, unknown>;
11
+ export declare function stripJsonSchemaConstraints(schema: JsonRecord): JsonRecord;
12
+ export {};