@contextrail/code-review-agent 0.1.1-alpha.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 (84) hide show
  1. package/LICENSE +26 -0
  2. package/MODEL_RECOMMENDATIONS.md +178 -0
  3. package/README.md +177 -0
  4. package/dist/config/defaults.d.ts +72 -0
  5. package/dist/config/defaults.js +113 -0
  6. package/dist/config/index.d.ts +34 -0
  7. package/dist/config/index.js +89 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +603 -0
  10. package/dist/llm/factory.d.ts +21 -0
  11. package/dist/llm/factory.js +50 -0
  12. package/dist/llm/index.d.ts +3 -0
  13. package/dist/llm/index.js +2 -0
  14. package/dist/llm/service.d.ts +38 -0
  15. package/dist/llm/service.js +191 -0
  16. package/dist/llm/types.d.ts +119 -0
  17. package/dist/llm/types.js +1 -0
  18. package/dist/logging/logger.d.ts +9 -0
  19. package/dist/logging/logger.js +52 -0
  20. package/dist/mcp/client.d.ts +429 -0
  21. package/dist/mcp/client.js +173 -0
  22. package/dist/mcp/mcp-tools.d.ts +292 -0
  23. package/dist/mcp/mcp-tools.js +40 -0
  24. package/dist/mcp/token-validation.d.ts +31 -0
  25. package/dist/mcp/token-validation.js +57 -0
  26. package/dist/mcp/tools-provider.d.ts +18 -0
  27. package/dist/mcp/tools-provider.js +24 -0
  28. package/dist/observability/index.d.ts +2 -0
  29. package/dist/observability/index.js +1 -0
  30. package/dist/observability/metrics.d.ts +48 -0
  31. package/dist/observability/metrics.js +86 -0
  32. package/dist/orchestrator/agentic-orchestrator.d.ts +29 -0
  33. package/dist/orchestrator/agentic-orchestrator.js +136 -0
  34. package/dist/orchestrator/prompts.d.ts +25 -0
  35. package/dist/orchestrator/prompts.js +98 -0
  36. package/dist/orchestrator/validation.d.ts +2 -0
  37. package/dist/orchestrator/validation.js +7 -0
  38. package/dist/orchestrator/writer.d.ts +4 -0
  39. package/dist/orchestrator/writer.js +17 -0
  40. package/dist/output/aggregator.d.ts +30 -0
  41. package/dist/output/aggregator.js +132 -0
  42. package/dist/output/prompts.d.ts +32 -0
  43. package/dist/output/prompts.js +153 -0
  44. package/dist/output/schema.d.ts +1515 -0
  45. package/dist/output/schema.js +224 -0
  46. package/dist/output/writer.d.ts +31 -0
  47. package/dist/output/writer.js +120 -0
  48. package/dist/review-inputs/chunking.d.ts +29 -0
  49. package/dist/review-inputs/chunking.js +113 -0
  50. package/dist/review-inputs/diff-summary.d.ts +52 -0
  51. package/dist/review-inputs/diff-summary.js +83 -0
  52. package/dist/review-inputs/file-patterns.d.ts +40 -0
  53. package/dist/review-inputs/file-patterns.js +182 -0
  54. package/dist/review-inputs/filtering.d.ts +31 -0
  55. package/dist/review-inputs/filtering.js +53 -0
  56. package/dist/review-inputs/git-diff-provider.d.ts +2 -0
  57. package/dist/review-inputs/git-diff-provider.js +42 -0
  58. package/dist/review-inputs/index.d.ts +46 -0
  59. package/dist/review-inputs/index.js +122 -0
  60. package/dist/review-inputs/path-validation.d.ts +10 -0
  61. package/dist/review-inputs/path-validation.js +37 -0
  62. package/dist/review-inputs/surrounding-context.d.ts +35 -0
  63. package/dist/review-inputs/surrounding-context.js +180 -0
  64. package/dist/review-inputs/triage.d.ts +57 -0
  65. package/dist/review-inputs/triage.js +81 -0
  66. package/dist/reviewers/executor.d.ts +41 -0
  67. package/dist/reviewers/executor.js +357 -0
  68. package/dist/reviewers/findings-merge.d.ts +9 -0
  69. package/dist/reviewers/findings-merge.js +131 -0
  70. package/dist/reviewers/iteration.d.ts +17 -0
  71. package/dist/reviewers/iteration.js +95 -0
  72. package/dist/reviewers/persistence.d.ts +17 -0
  73. package/dist/reviewers/persistence.js +55 -0
  74. package/dist/reviewers/progress-tracker.d.ts +115 -0
  75. package/dist/reviewers/progress-tracker.js +194 -0
  76. package/dist/reviewers/prompt.d.ts +42 -0
  77. package/dist/reviewers/prompt.js +246 -0
  78. package/dist/reviewers/tool-call-tracker.d.ts +18 -0
  79. package/dist/reviewers/tool-call-tracker.js +40 -0
  80. package/dist/reviewers/types.d.ts +12 -0
  81. package/dist/reviewers/types.js +1 -0
  82. package/dist/reviewers/validation-rules.d.ts +27 -0
  83. package/dist/reviewers/validation-rules.js +189 -0
  84. package/package.json +79 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * File pattern configuration from prompt metadata
3
+ */
4
+ export type FilePatterns = {
5
+ include?: string[];
6
+ exclude?: string[];
7
+ };
8
+ /**
9
+ * Check if a file matches file patterns from prompt metadata.
10
+ *
11
+ * Security improvements:
12
+ * - Validates and sanitizes patterns to prevent path traversal
13
+ * - Error handling for malformed glob patterns
14
+ * - Limits pattern complexity
15
+ *
16
+ * @param filename - File path to check
17
+ * @param filePatterns - File pattern configuration from prompt metadata
18
+ * @returns True if file should be included for review
19
+ */
20
+ export declare function fileMatchesPatterns(filename: string, filePatterns?: FilePatterns): boolean;
21
+ /**
22
+ * Filter files based on file patterns from prompt metadata.
23
+ *
24
+ * @param files - Array of file paths to filter
25
+ * @param filePatterns - File pattern configuration from prompt metadata
26
+ * @returns Filtered array of file paths
27
+ */
28
+ export declare function filterFilesByPatterns(files: string[], filePatterns?: FilePatterns): string[];
29
+ /**
30
+ * Filter diff inputs by file patterns from prompt metadata.
31
+ *
32
+ * @param files - Array of file paths
33
+ * @param diffs - Record of file paths to diff content
34
+ * @param filePatterns - File pattern configuration from prompt metadata
35
+ * @returns Filtered files and diffs
36
+ */
37
+ export declare function filterDiffInputsByPatterns(files: string[], diffs: Record<string, string>, filePatterns?: FilePatterns): {
38
+ files: string[];
39
+ diffs: Record<string, string>;
40
+ };
@@ -0,0 +1,182 @@
1
+ import { Minimatch } from 'minimatch';
2
+ /**
3
+ * Security constants for input validation
4
+ */
5
+ const MAX_STRING_LENGTH = 10000;
6
+ /**
7
+ * Cache for compiled minimatch patterns to avoid recompilation
8
+ */
9
+ const minimatchCache = new Map();
10
+ /**
11
+ * Check if a pattern contains dangerous characters using safe string methods
12
+ */
13
+ function hasDangerousPatternChars(pattern) {
14
+ // Use simple string methods instead of regex to avoid ReDoS
15
+ return (pattern.includes('..') || // Path traversal
16
+ pattern.startsWith('/') || // Absolute path
17
+ pattern.includes('\\') || // Backslash (Windows path separator)
18
+ pattern.includes('\x00') // Null byte
19
+ );
20
+ }
21
+ /**
22
+ * Validate and sanitize a file pattern to prevent path traversal
23
+ */
24
+ function sanitizeFilePattern(pattern) {
25
+ if (!pattern || typeof pattern !== 'string') {
26
+ return null;
27
+ }
28
+ // Check for dangerous patterns using safe string methods (no ReDoS risk)
29
+ if (hasDangerousPatternChars(pattern)) {
30
+ return null;
31
+ }
32
+ // Limit length to prevent ReDoS in minimatch
33
+ if (pattern.length > MAX_STRING_LENGTH) {
34
+ return pattern.substring(0, MAX_STRING_LENGTH);
35
+ }
36
+ return pattern;
37
+ }
38
+ /**
39
+ * Get or compile a minimatch pattern with caching
40
+ */
41
+ function getCompiledPattern(pattern) {
42
+ const cached = minimatchCache.get(pattern);
43
+ if (cached !== undefined) {
44
+ return cached;
45
+ }
46
+ try {
47
+ const compiled = new Minimatch(pattern, { dot: true, nocomment: true });
48
+ minimatchCache.set(pattern, compiled);
49
+ return compiled;
50
+ }
51
+ catch {
52
+ minimatchCache.set(pattern, null);
53
+ return null;
54
+ }
55
+ }
56
+ /**
57
+ * Normalize file path separators for cross-platform compatibility.
58
+ * Converts Windows backslashes to forward slashes for consistent glob matching.
59
+ */
60
+ function normalizePath(path) {
61
+ return path.replace(/\\/g, '/');
62
+ }
63
+ /**
64
+ * Safely match a file against a compiled pattern
65
+ */
66
+ function safeMinimatch(filename, pattern) {
67
+ const compiled = getCompiledPattern(pattern);
68
+ if (!compiled) {
69
+ return false;
70
+ }
71
+ try {
72
+ // Normalize filename to use forward slashes for consistent matching
73
+ const normalizedFilename = normalizePath(filename);
74
+ return compiled.match(normalizedFilename);
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Check if a file matches file patterns from prompt metadata.
82
+ *
83
+ * Security improvements:
84
+ * - Validates and sanitizes patterns to prevent path traversal
85
+ * - Error handling for malformed glob patterns
86
+ * - Limits pattern complexity
87
+ *
88
+ * @param filename - File path to check
89
+ * @param filePatterns - File pattern configuration from prompt metadata
90
+ * @returns True if file should be included for review
91
+ */
92
+ export function fileMatchesPatterns(filename, filePatterns) {
93
+ // If no file patterns, include all files (backward compatible)
94
+ if (!filePatterns) {
95
+ return true;
96
+ }
97
+ // Validate filename
98
+ if (!filename || typeof filename !== 'string') {
99
+ return false;
100
+ }
101
+ const { include, exclude } = filePatterns;
102
+ try {
103
+ // Check exclude patterns first (higher priority)
104
+ if (exclude && Array.isArray(exclude) && exclude.length > 0) {
105
+ for (const pattern of exclude) {
106
+ const sanitized = sanitizeFilePattern(pattern);
107
+ if (!sanitized) {
108
+ continue; // Skip invalid patterns
109
+ }
110
+ if (safeMinimatch(filename, sanitized)) {
111
+ return false;
112
+ }
113
+ }
114
+ }
115
+ // If include patterns are specified, file must match at least one
116
+ if (include && Array.isArray(include) && include.length > 0) {
117
+ let hasMatch = false;
118
+ let hasValidPattern = false;
119
+ for (const pattern of include) {
120
+ const sanitized = sanitizeFilePattern(pattern);
121
+ if (!sanitized) {
122
+ continue; // Skip invalid patterns
123
+ }
124
+ hasValidPattern = true;
125
+ if (safeMinimatch(filename, sanitized)) {
126
+ hasMatch = true;
127
+ break;
128
+ }
129
+ }
130
+ // Fail-safe: if all patterns were invalid, include the file
131
+ if (!hasValidPattern) {
132
+ return true;
133
+ }
134
+ if (!hasMatch) {
135
+ return false;
136
+ }
137
+ }
138
+ return true;
139
+ }
140
+ catch {
141
+ // Fail-safe: include file if pattern matching fails
142
+ return true;
143
+ }
144
+ }
145
+ /**
146
+ * Filter files based on file patterns from prompt metadata.
147
+ *
148
+ * @param files - Array of file paths to filter
149
+ * @param filePatterns - File pattern configuration from prompt metadata
150
+ * @returns Filtered array of file paths
151
+ */
152
+ export function filterFilesByPatterns(files, filePatterns) {
153
+ if (!filePatterns) {
154
+ return files;
155
+ }
156
+ return files.filter((file) => fileMatchesPatterns(file, filePatterns));
157
+ }
158
+ /**
159
+ * Filter diff inputs by file patterns from prompt metadata.
160
+ *
161
+ * @param files - Array of file paths
162
+ * @param diffs - Record of file paths to diff content
163
+ * @param filePatterns - File pattern configuration from prompt metadata
164
+ * @returns Filtered files and diffs
165
+ */
166
+ export function filterDiffInputsByPatterns(files, diffs, filePatterns) {
167
+ const filteredFiles = filterFilesByPatterns(files, filePatterns);
168
+ // Filter diffs to only include files that passed filtering
169
+ const filteredDiffs = {};
170
+ for (const file of filteredFiles) {
171
+ if (file in diffs) {
172
+ if (!diffs[file]) {
173
+ continue;
174
+ }
175
+ filteredDiffs[file] = diffs[file];
176
+ }
177
+ }
178
+ return {
179
+ files: filteredFiles,
180
+ diffs: filteredDiffs,
181
+ };
182
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Configuration for file filtering.
3
+ */
4
+ export type FilteringConfig = {
5
+ /**
6
+ * Patterns to exclude from review.
7
+ * Uses glob patterns (minimatch) for secure matching.
8
+ * Default: DEFAULT_EXCLUDE_PATTERNS
9
+ */
10
+ excludePatterns?: ReadonlyArray<string>;
11
+ };
12
+ /**
13
+ * Filter files based on exclude patterns.
14
+ *
15
+ * @param files - Array of file paths to filter
16
+ * @param config - Filtering configuration
17
+ * @returns Filtered array of file paths
18
+ */
19
+ export declare const filterFiles: (files: string[], config?: FilteringConfig) => string[];
20
+ /**
21
+ * Filter diff inputs by removing excluded files and their diffs.
22
+ *
23
+ * @param files - Array of file paths
24
+ * @param diffs - Record of file paths to diff content
25
+ * @param config - Filtering configuration
26
+ * @returns Filtered files and diffs
27
+ */
28
+ export declare const filterDiffInputs: (files: string[], diffs: Record<string, string>, config?: FilteringConfig) => {
29
+ files: string[];
30
+ diffs: Record<string, string>;
31
+ };
@@ -0,0 +1,53 @@
1
+ import { DEFAULT_EXCLUDE_PATTERNS } from '../config/defaults.js';
2
+ import { fileMatchesPatterns } from './file-patterns.js';
3
+ /**
4
+ * Check if a file path matches any exclude pattern.
5
+ * Uses minimatch for secure glob pattern matching (prevents ReDoS).
6
+ *
7
+ * @param filePath - File path to check (relative or absolute)
8
+ * @param patterns - Glob patterns to match against
9
+ * @returns True if file should be excluded
10
+ */
11
+ const matchesExcludePattern = (filePath, patterns) => {
12
+ // Convert patterns to FilePatterns format for consistent matching
13
+ const filePatterns = {
14
+ exclude: [...patterns],
15
+ };
16
+ // Use fileMatchesPatterns which handles minimatch safely
17
+ // If file matches exclude patterns, it should be excluded
18
+ return !fileMatchesPatterns(filePath, filePatterns);
19
+ };
20
+ /**
21
+ * Filter files based on exclude patterns.
22
+ *
23
+ * @param files - Array of file paths to filter
24
+ * @param config - Filtering configuration
25
+ * @returns Filtered array of file paths
26
+ */
27
+ export const filterFiles = (files, config = {}) => {
28
+ const patterns = config.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS;
29
+ return files.filter((file) => !matchesExcludePattern(file, patterns));
30
+ };
31
+ /**
32
+ * Filter diff inputs by removing excluded files and their diffs.
33
+ *
34
+ * @param files - Array of file paths
35
+ * @param diffs - Record of file paths to diff content
36
+ * @param config - Filtering configuration
37
+ * @returns Filtered files and diffs
38
+ */
39
+ export const filterDiffInputs = (files, diffs, config = {}) => {
40
+ const filteredFiles = filterFiles(files, config);
41
+ // Filter diffs to only include files that passed filtering
42
+ const filteredDiffs = {};
43
+ for (const file of filteredFiles) {
44
+ const diff = diffs[file];
45
+ if (diff !== undefined) {
46
+ filteredDiffs[file] = diff;
47
+ }
48
+ }
49
+ return {
50
+ files: filteredFiles,
51
+ diffs: filteredDiffs,
52
+ };
53
+ };
@@ -0,0 +1,2 @@
1
+ import type { DiffProvider } from './index.js';
2
+ export declare const gitDiffProvider: DiffProvider;
@@ -0,0 +1,42 @@
1
+ import simpleGit from 'simple-git';
2
+ import { validateAndNormalizePath } from './path-validation.js';
3
+ const parseFileList = (filesRaw) => {
4
+ return filesRaw
5
+ .split('\n')
6
+ .map((file) => file.trim())
7
+ .filter(Boolean);
8
+ };
9
+ export const gitDiffProvider = async ({ repoPath, from, to }) => {
10
+ try {
11
+ const git = simpleGit(repoPath);
12
+ const topLevel = (await git.revparse(['--show-toplevel'])).trim();
13
+ const topGit = simpleGit(topLevel);
14
+ // Use array arguments to prevent command injection
15
+ // simple-git handles array args safely by treating each element as a separate argument
16
+ const filesRaw = await topGit.raw(['diff', '--name-only', `${from}..${to}`]);
17
+ const filesRawList = parseFileList(filesRaw);
18
+ const files = [];
19
+ const diffs = {};
20
+ // Validate and normalize all file paths to prevent path traversal
21
+ for (const file of filesRawList) {
22
+ try {
23
+ const validatedPath = validateAndNormalizePath(file, topLevel);
24
+ files.push(validatedPath);
25
+ // Use array arguments for git diff command to prevent injection
26
+ const diff = await topGit.raw(['diff', '--no-color', `${from}..${to}`, '--', validatedPath]);
27
+ diffs[validatedPath] = diff;
28
+ }
29
+ catch (pathError) {
30
+ // Skip files that fail path validation (path traversal attempts)
31
+ // Log error but continue processing other files
32
+ const message = pathError instanceof Error ? pathError.message : 'Path validation failed';
33
+ throw new Error(`Invalid file path "${file}": ${message}`);
34
+ }
35
+ }
36
+ return { files, diffs };
37
+ }
38
+ catch (error) {
39
+ const message = error instanceof Error ? error.message : 'Unknown error';
40
+ throw new Error(`Failed to compute git diff: ${message}`);
41
+ }
42
+ };
@@ -0,0 +1,46 @@
1
+ import { type FilteringConfig } from './filtering.js';
2
+ import type { SurroundingContextConfig } from './surrounding-context.js';
3
+ export { triagePr, type TriageConfig, type TriageResult } from './triage.js';
4
+ export type ReviewInputMode = 'diff' | 'file-list';
5
+ export type DiffReviewInputs = {
6
+ mode: 'diff';
7
+ files: string[];
8
+ diffs: Record<string, string>;
9
+ context?: Record<string, string>;
10
+ };
11
+ export type FileListReviewInputs = {
12
+ mode: 'file-list';
13
+ files: string[];
14
+ context?: Record<string, string>;
15
+ };
16
+ export type ReviewInputs = DiffReviewInputs | FileListReviewInputs;
17
+ export type DiffInput = {
18
+ mode: 'diff';
19
+ repoPath: string;
20
+ from: string;
21
+ to: string;
22
+ filtering?: FilteringConfig;
23
+ surroundingContext?: SurroundingContextConfig;
24
+ };
25
+ export type FileListInput = {
26
+ mode: 'file-list';
27
+ files: string[];
28
+ basePath?: string;
29
+ filtering?: FilteringConfig;
30
+ surroundingContext?: SurroundingContextConfig;
31
+ };
32
+ export type ReviewInputOptions = DiffInput | FileListInput;
33
+ export declare const isDiffInputs: (inputs: ReviewInputs) => inputs is DiffReviewInputs;
34
+ export declare const isFileListInputs: (inputs: ReviewInputs) => inputs is FileListReviewInputs;
35
+ export type DiffProvider = (params: {
36
+ repoPath: string;
37
+ from: string;
38
+ to: string;
39
+ }) => Promise<{
40
+ files: string[];
41
+ diffs: Record<string, string>;
42
+ }>;
43
+ export declare const buildReviewInputs: (options: ReviewInputOptions, deps?: {
44
+ diffProvider?: DiffProvider;
45
+ }) => Promise<ReviewInputs>;
46
+ export declare const persistReviewInputs: (inputs: ReviewInputs, outputDir: string) => Promise<void>;
@@ -0,0 +1,122 @@
1
+ import { access, mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { gitDiffProvider } from './git-diff-provider.js';
4
+ import { filterDiffInputs, filterFiles } from './filtering.js';
5
+ export { triagePr } from './triage.js';
6
+ export const isDiffInputs = (inputs) => inputs.mode === 'diff';
7
+ export const isFileListInputs = (inputs) => inputs.mode === 'file-list';
8
+ const resolveFilePath = (file, basePath) => {
9
+ if (!basePath) {
10
+ return file;
11
+ }
12
+ if (path.isAbsolute(file)) {
13
+ return file;
14
+ }
15
+ return path.resolve(basePath, file);
16
+ };
17
+ const toRelativeIfInsideBase = (filePath, basePath) => {
18
+ if (!basePath) {
19
+ return filePath;
20
+ }
21
+ const relative = path.relative(basePath, filePath);
22
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
23
+ return filePath;
24
+ }
25
+ return relative;
26
+ };
27
+ const validateFileList = async (files, basePath) => {
28
+ if (files.length === 0) {
29
+ throw new Error('File list must include at least one file.');
30
+ }
31
+ const missing = [];
32
+ for (const file of files) {
33
+ try {
34
+ const resolved = resolveFilePath(file, basePath);
35
+ await access(resolved);
36
+ }
37
+ catch {
38
+ missing.push(file);
39
+ }
40
+ }
41
+ if (missing.length > 0) {
42
+ throw new Error(`Files not found: ${missing.join(', ')}`);
43
+ }
44
+ };
45
+ const normalizeFiles = (files) => {
46
+ return files.map((file) => file.trim()).filter(Boolean);
47
+ };
48
+ export const buildReviewInputs = async (options, deps) => {
49
+ if (options.mode === 'diff') {
50
+ const diffProvider = deps?.diffProvider ?? gitDiffProvider;
51
+ const result = await diffProvider({
52
+ repoPath: options.repoPath,
53
+ from: options.from,
54
+ to: options.to,
55
+ });
56
+ const normalizedFiles = normalizeFiles(result.files);
57
+ const { files, diffs } = filterDiffInputs(normalizedFiles, result.diffs, options.filtering);
58
+ if (files.length === 0) {
59
+ throw new Error('Diff mode produced no files to review after filtering.');
60
+ }
61
+ const inputs = {
62
+ mode: 'diff',
63
+ files,
64
+ diffs,
65
+ };
66
+ // Extract surrounding context if configured
67
+ if (options.surroundingContext?.enabled !== false) {
68
+ const { extractSurroundingContext } = await import('./surrounding-context.js');
69
+ const context = await extractSurroundingContext(inputs, {
70
+ ...options.surroundingContext,
71
+ basePath: options.surroundingContext?.basePath ?? options.repoPath,
72
+ });
73
+ if (Object.keys(context).length > 0) {
74
+ inputs.context = context;
75
+ }
76
+ }
77
+ return inputs;
78
+ }
79
+ const normalizedFiles = normalizeFiles(options.files);
80
+ const files = filterFiles(normalizedFiles, options.filtering);
81
+ if (files.length === 0) {
82
+ throw new Error('File list produced no files to review after filtering.');
83
+ }
84
+ await validateFileList(files, options.basePath);
85
+ const relativeFiles = files.map((file) => toRelativeIfInsideBase(resolveFilePath(file, options.basePath), options.basePath));
86
+ const inputs = {
87
+ mode: 'file-list',
88
+ files: relativeFiles,
89
+ };
90
+ // Extract surrounding context if configured
91
+ // For file-list mode, basePath is required for context extraction
92
+ if (options.surroundingContext?.enabled !== false) {
93
+ // Skip context extraction if basePath is missing in file-list mode
94
+ if (options.mode === 'file-list' && !options.basePath) {
95
+ // Context extraction requires basePath for file-list mode, skip it
96
+ }
97
+ else {
98
+ const { extractSurroundingContext } = await import('./surrounding-context.js');
99
+ const context = await extractSurroundingContext(inputs, {
100
+ ...options.surroundingContext,
101
+ basePath: options.basePath,
102
+ });
103
+ if (Object.keys(context).length > 0) {
104
+ inputs.context = context;
105
+ }
106
+ }
107
+ }
108
+ return inputs;
109
+ };
110
+ export const persistReviewInputs = async (inputs, outputDir) => {
111
+ await mkdir(outputDir, { recursive: true });
112
+ const filesPayload = JSON.stringify({ mode: inputs.mode, files: inputs.files }, null, 2);
113
+ await writeFile(path.join(outputDir, 'files.json'), filesPayload);
114
+ if (inputs.mode !== 'diff') {
115
+ return;
116
+ }
117
+ for (const [filePath, diff] of Object.entries(inputs.diffs)) {
118
+ const diffPath = path.join(outputDir, 'diff', `${filePath}.diff`);
119
+ await mkdir(path.dirname(diffPath), { recursive: true });
120
+ await writeFile(diffPath, diff);
121
+ }
122
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Validates that a file path is safe and within the repository boundary.
3
+ * Prevents path traversal attacks by ensuring resolved paths stay within basePath.
4
+ *
5
+ * @param filePath - Relative file path from git or file list
6
+ * @param basePath - Base directory (repository root) to validate against
7
+ * @returns Normalized, validated relative path
8
+ * @throws Error if path traversal is detected or path is outside basePath
9
+ */
10
+ export declare const validateAndNormalizePath: (filePath: string, basePath: string) => string;
@@ -0,0 +1,37 @@
1
+ import path from 'node:path';
2
+ import { realpathSync } from 'node:fs';
3
+ /**
4
+ * Validates that a file path is safe and within the repository boundary.
5
+ * Prevents path traversal attacks by ensuring resolved paths stay within basePath.
6
+ *
7
+ * @param filePath - Relative file path from git or file list
8
+ * @param basePath - Base directory (repository root) to validate against
9
+ * @returns Normalized, validated relative path
10
+ * @throws Error if path traversal is detected or path is outside basePath
11
+ */
12
+ export const validateAndNormalizePath = (filePath, basePath) => {
13
+ // Normalize the file path (remove . and .. segments)
14
+ const normalized = path.normalize(filePath);
15
+ // Resolve against basePath to get absolute path
16
+ const resolved = path.resolve(basePath, normalized);
17
+ // Resolve basePath to absolute (handles symlinks)
18
+ const baseResolved = path.resolve(basePath);
19
+ // Check if resolved path is within basePath
20
+ // Use realpathSync to resolve symlinks and ensure we're checking actual filesystem paths
21
+ try {
22
+ const resolvedReal = realpathSync(resolved);
23
+ const baseReal = realpathSync(baseResolved);
24
+ // Ensure the resolved path starts with the base path
25
+ if (!resolvedReal.startsWith(baseReal + path.sep) && resolvedReal !== baseReal) {
26
+ throw new Error(`Path traversal detected: "${filePath}" resolves outside repository boundary`);
27
+ }
28
+ }
29
+ catch {
30
+ // If realpathSync fails, the file doesn't exist - but we still check the resolved path
31
+ if (!resolved.startsWith(baseResolved + path.sep) && resolved !== baseResolved) {
32
+ throw new Error(`Path traversal detected: "${filePath}" resolves outside repository boundary`);
33
+ }
34
+ }
35
+ // Return normalized relative path
36
+ return normalized;
37
+ };
@@ -0,0 +1,35 @@
1
+ import type { ReviewInputs } from './index.js';
2
+ /**
3
+ * Configuration for surrounding context extraction.
4
+ */
5
+ export type SurroundingContextConfig = {
6
+ /**
7
+ * Maximum tokens to use for surrounding context per file.
8
+ * Default: 20000 (roughly 80KB of text)
9
+ */
10
+ maxTokensPerFile?: number;
11
+ /**
12
+ * Number of lines of context to include before and after changed lines.
13
+ * Only used in diff mode when extracting windowed context.
14
+ * Default: 10
15
+ */
16
+ contextLines?: number;
17
+ /**
18
+ * Whether to extract surrounding context.
19
+ * Default: true
20
+ */
21
+ enabled?: boolean;
22
+ /**
23
+ * Base path for resolving file paths (required for file-list mode).
24
+ */
25
+ basePath?: string;
26
+ };
27
+ /**
28
+ * Extract surrounding code context for review inputs.
29
+ * Returns a map of file paths to their context (full file or windowed excerpt).
30
+ *
31
+ * @param inputs - Review inputs (diff or file-list)
32
+ * @param config - Context extraction configuration
33
+ * @returns Map of file paths to context strings (null values indicate unavailable context)
34
+ */
35
+ export declare const extractSurroundingContext: (inputs: ReviewInputs, config?: SurroundingContextConfig) => Promise<Record<string, string>>;